From 5f5ab701892f764b0d6f952081fe68cea8da2b40 Mon Sep 17 00:00:00 2001 From: Discostu Date: Thu, 14 May 2026 22:40:34 +0200 Subject: [PATCH 01/16] feat: add Aptos HDWallet support (core + native adapter) - Add hdwallet-core/src/aptos.ts with AptosWallet interfaces - Add SLIP-44 637 for Aptos in utils.ts - Add supportsAptos/infoAptos in wallet.ts - Add hdwallet-native AptosAdapter (Ed25519 + SHA3-256) - Add MixinNativeAptosWallet/MixinNativeAptosWalletInfo - Wire into native.ts (initialize, wipe, describePath, mixins) Local only - no PR --- packages/hdwallet-core/src/aptos.ts | 119 ++++++++++++++++++ packages/hdwallet-core/src/index.ts | 1 + packages/hdwallet-core/src/utils.ts | 1 + packages/hdwallet-core/src/wallet.ts | 9 +- packages/hdwallet-native/src/aptos.ts | 64 ++++++++++ .../src/crypto/isolation/adapters/aptos.ts | 64 ++++++++++ packages/hdwallet-native/src/native.ts | 9 ++ 7 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 packages/hdwallet-core/src/aptos.ts create mode 100644 packages/hdwallet-native/src/aptos.ts create mode 100644 packages/hdwallet-native/src/crypto/isolation/adapters/aptos.ts diff --git a/packages/hdwallet-core/src/aptos.ts b/packages/hdwallet-core/src/aptos.ts new file mode 100644 index 00000000000..d919802d762 --- /dev/null +++ b/packages/hdwallet-core/src/aptos.ts @@ -0,0 +1,119 @@ +import { addressNListToBIP32, slip44ByCoin } from './utils' +import type { BIP32Path, HDWallet, HDWalletInfo, PathDescription } from './wallet' + +export interface AptosGetAddress { + addressNList: BIP32Path + showDisplay?: boolean +} + +export interface AptosSignTx { + addressNList: BIP32Path + /** Raw transaction bytes to sign (BCS serialized) */ + txBytes: Uint8Array +} + +export interface AptosSignedTx { + signature: string + publicKey: string + txBytes: Uint8Array +} + +export interface AptosGetAccountPaths { + accountIdx: number +} + +export interface AptosAccountPath { + addressNList: BIP32Path +} + +export interface AptosWalletInfo extends HDWalletInfo { + readonly _supportsAptosInfo: boolean + + /** + * Returns a list of bip32 paths for a given account index in preferred order + * from most to least preferred. + */ + aptosGetAccountPaths(msg: AptosGetAccountPaths): AptosAccountPath[] + + /** + * Returns the "next" account path, if any. + */ + aptosNextAccountPath(msg: AptosAccountPath): AptosAccountPath | undefined +} + +export interface AptosWallet extends AptosWalletInfo, HDWallet { + readonly _supportsAptos: boolean + + aptosGetAddress(msg: AptosGetAddress): Promise + aptosSignTx(msg: AptosSignTx): Promise +} + +export function aptosDescribePath(path: BIP32Path): PathDescription { + const pathStr = addressNListToBIP32(path) + const unknown: PathDescription = { + verbose: pathStr, + coin: 'Aptos', + isKnown: false, + } + + // Aptos uses m/44'/637'/0'/0'/0' - standard BIP44 with SLIP-44 = 637 + // https://github.com/aptos-labs/aptos-core/blob/main/sdk/src/wallet.rs + const slip44 = slip44ByCoin('Aptos') + if (slip44 === undefined) return unknown + if (path.length != 5) return unknown + if (path[0] != 0x80000000 + 44) return unknown + if (path[1] != 0x80000000 + slip44) return unknown + if (path[2] != 0x80000000 + 0) return unknown + if (path[3] != 0x80000000 + 0) return unknown + if ((path[4] & 0x80000000) >>> 0 !== 0) return unknown + + const index = path[4] & 0x7fffffff + return { + verbose: `Aptos Account #${index}`, + accountIdx: index, + wholeAccount: true, + coin: 'Aptos', + isKnown: true, + } +} + +// Aptos uses standard BIP44 derivation: m/44'/637'/0'/0'/ +// https://github.com/satoshilabs/slips/blob/master/slip-0044.md (637 = Aptos) +export function aptosGetAccountPaths(msg: AptosGetAccountPaths): AptosAccountPath[] { + const slip44 = slip44ByCoin('Aptos') + if (slip44 === undefined) return [] + return [ + { + addressNList: [ + 0x80000000 + 44, + 0x80000000 + slip44, + 0x80000000 + 0, + 0x80000000 + 0, + msg.accountIdx, + ], + }, + ] +} + +export function aptosNextAccountPath(msg: AptosAccountPath): AptosAccountPath | undefined { + const slip44 = slip44ByCoin('Aptos') + if (slip44 === undefined) return undefined + // Only return next if the path looks like m/44'/637'/0'/0'/n + if (msg.addressNList.length !== 5) return undefined + if (msg.addressNList[0] !== 0x80000000 + 44) return undefined + if (msg.addressNList[1] !== 0x80000000 + slip44) return undefined + if (msg.addressNList[2] !== 0x80000000 + 0) return undefined + if (msg.addressNList[3] !== 0x80000000 + 0) return undefined + if ((msg.addressNList[4] & 0x80000000) >>> 0 !== 0) return undefined + + const nextIndex = (msg.addressNList[4] & 0x7fffffff) + 1 + return { + addressNList: [ + 0x80000000 + 44, + 0x80000000 + slip44, + 0x80000000 + 0, + 0x80000000 + 0, + nextIndex, + ], + } +} diff --git a/packages/hdwallet-core/src/index.ts b/packages/hdwallet-core/src/index.ts index fd27c6c4038..a55b5974b12 100644 --- a/packages/hdwallet-core/src/index.ts +++ b/packages/hdwallet-core/src/index.ts @@ -21,6 +21,7 @@ export * from './starknet' export * from './sui' export * from './near' export * from './ton' +export * from './aptos' export * from './transport' export * from './tron' export * from './utils' diff --git a/packages/hdwallet-core/src/utils.ts b/packages/hdwallet-core/src/utils.ts index c0fc9e1f4cf..0989b809983 100644 --- a/packages/hdwallet-core/src/utils.ts +++ b/packages/hdwallet-core/src/utils.ts @@ -164,6 +164,7 @@ export const slip44Table = Object.freeze({ Sui: 784, Near: 397, Ton: 607, + Aptos: 637, // EVM chains all use the same SLIP44 Ethereum: 60, Avalanche: 60, diff --git a/packages/hdwallet-core/src/wallet.ts b/packages/hdwallet-core/src/wallet.ts index 97ec2596115..b4827662131 100644 --- a/packages/hdwallet-core/src/wallet.ts +++ b/packages/hdwallet-core/src/wallet.ts @@ -18,6 +18,7 @@ import type { SuiWallet, SuiWalletInfo } from './sui' import type { TerraWallet, TerraWalletInfo } from './terra' import type { ThorchainWallet, ThorchainWalletInfo } from './thorchain' import type { TonWallet, TonWalletInfo } from './ton' +import type { AptosWallet, AptosWalletInfo } from './aptos' import type { Transport } from './transport' import type { TronWallet, TronWalletInfo } from './tron' @@ -401,8 +402,12 @@ export function infoTon(info: HDWalletInfo): info is TonWalletInfo { return isObject(info) && (info as any)._supportsTonInfo } -export function supportsDebugLink(wallet: HDWallet): wallet is DebugLinkWallet { - return isObject(wallet) && (wallet as any)._supportsDebugLink +export function supportsAptos(wallet: HDWallet): wallet is AptosWallet { + return isObject(wallet) && (wallet as any)._supportsAptos +} + +export function infoAptos(info: HDWalletInfo): info is AptosWalletInfo { + return isObject(info) && (info as any)._supportsAptosInfo } export function isMetaMask(wallet: HDWallet | null): boolean { diff --git a/packages/hdwallet-native/src/aptos.ts b/packages/hdwallet-native/src/aptos.ts new file mode 100644 index 00000000000..ac5ca6d4a8d --- /dev/null +++ b/packages/hdwallet-native/src/aptos.ts @@ -0,0 +1,64 @@ +import * as core from '@shapeshiftoss/hdwallet-core' + +import { Isolation } from './crypto' +import { AptosAdapter } from './crypto/isolation/adapters/aptos' +import type { NativeHDWalletBase } from './native' + +export function MixinNativeAptosWalletInfo>( + Base: TBase, +) { + // eslint-disable-next-line @typescript-eslint/no-shadow + return class MixinNativeAptosWalletInfo extends Base implements core.AptosWalletInfo { + readonly _supportsAptosInfo = true + + aptosGetAccountPaths(msg: core.AptosGetAccountPaths): core.AptosAccountPath[] { + return core.aptosGetAccountPaths(msg) + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + aptosNextAccountPath(_msg: core.AptosAccountPath): core.AptosAccountPath | undefined { + throw new Error('Method not implemented') + } + } +} + +export function MixinNativeAptosWallet>( + Base: TBase, +) { + // eslint-disable-next-line @typescript-eslint/no-shadow + return class MixinNativeAptosWallet extends Base { + readonly _supportsAptos = true + + aptosAdapter: AptosAdapter | undefined + + async aptosInitializeWallet(ed25519MasterKey: Isolation.Core.Ed25519.Node): Promise { + const nodeAdapter = new Isolation.Adapters.Ed25519(ed25519MasterKey) + this.aptosAdapter = new AptosAdapter(nodeAdapter) + } + + aptosWipe() { + this.aptosAdapter = undefined + } + + async aptosGetAddress(msg: core.AptosGetAddress): Promise { + return this.needsMnemonic(!!this.aptosAdapter, () => { + return this.aptosAdapter!.getAddress(msg.addressNList) + }) + } + + async aptosSignTx(msg: core.AptosSignTx): Promise { + return this.needsMnemonic(!!this.aptosAdapter, async () => { + const { signature, publicKey } = await this.aptosAdapter!.signTransaction( + msg.txBytes, + msg.addressNList, + ) + + return { + signature, + publicKey, + txBytes: msg.txBytes, + } + }) + } + } +} diff --git a/packages/hdwallet-native/src/crypto/isolation/adapters/aptos.ts b/packages/hdwallet-native/src/crypto/isolation/adapters/aptos.ts new file mode 100644 index 00000000000..b5010999fe6 --- /dev/null +++ b/packages/hdwallet-native/src/crypto/isolation/adapters/aptos.ts @@ -0,0 +1,64 @@ +import * as core from '@shapeshiftoss/hdwallet-core' + +import { Isolation } from './crypto' + +const ED25519_PUBLIC_KEY_SIZE = 32 + +/** + * Aptos uses Ed25519 signing with BIP44 derivation path m/44'/637'/0'/0'/0' + * Addresses are derived from the Ed25519 public key using SHA3-256 truncated to 32 bytes. + * The @aptos-labs/ts-sdk handles BCS serialization and transaction construction. + */ +export class AptosAdapter { + protected readonly nodeAdapter: Isolation.Adapters.Ed25519 + + constructor(nodeAdapter: Isolation.Adapters.Ed25519) { + this.nodeAdapter = nodeAdapter + } + + async getAddress(addressNList: core.BIP32Path): Promise { + const publicKey = await this.getPublicKeyRaw(addressNList) + + // Aptos address = SHA3-256 of the public key, prefixed with 0x + // For single-key accounts, the address is 0x + sha3_256(publicKey) + const { createHash } = await import('crypto') + const hash = createHash('sha3-256').update(publicKey).digest('hex') + return `0x${hash}` + } + + async getPublicKey(addressNList: core.BIP32Path): Promise { + const publicKey = await this.getPublicKeyRaw(addressNList) + return Buffer.from(publicKey).toString('hex') + } + + private async getPublicKeyRaw(addressNList: core.BIP32Path): Promise { + const nodeAdapter = await this.nodeAdapter.derivePath( + core.addressNListToHardenedBIP32(addressNList), + ) + const publicKey = await nodeAdapter.getPublicKey() + + if (publicKey.length !== ED25519_PUBLIC_KEY_SIZE) { + throw new Error(`Invalid Ed25519 public key size: ${publicKey.length}`) + } + + return Buffer.from(publicKey) + } + + async signTransaction( + txBytes: Uint8Array, + addressNList: core.BIP32Path, + ): Promise<{ signature: string; publicKey: string }> { + const nodeAdapter = await this.nodeAdapter.derivePath( + core.addressNListToHardenedBIP32(addressNList), + ) + const publicKey = await nodeAdapter.getPublicKey() + const signature = await nodeAdapter.node.sign(txBytes) + + return { + signature: Buffer.from(signature).toString('hex'), + publicKey: Buffer.from(publicKey).toString('hex'), + } + } +} + +export default AptosAdapter diff --git a/packages/hdwallet-native/src/native.ts b/packages/hdwallet-native/src/native.ts index 36c19f5fafb..c1695f4813c 100644 --- a/packages/hdwallet-native/src/native.ts +++ b/packages/hdwallet-native/src/native.ts @@ -22,6 +22,7 @@ import { MixinNativeSuiWallet, MixinNativeSuiWalletInfo } from './sui' import { MixinNativeTerraWallet, MixinNativeTerraWalletInfo } from './terra' import { MixinNativeThorchainWallet, MixinNativeThorchainWalletInfo } from './thorchain' import { MixinNativeTonWallet, MixinNativeTonWalletInfo } from './ton' +import { MixinNativeAptosWallet, MixinNativeAptosWalletInfo } from './aptos' import { MixinNativeTronWallet, MixinNativeTronWalletInfo } from './tron' export { NativeEvents } from './nativeEvents' @@ -133,6 +134,7 @@ class NativeHDWalletInfo MixinNativeStarknetWalletInfo( MixinNativeTronWalletInfo( MixinNativeTonWalletInfo( + MixinNativeAptosWalletInfo( MixinNativeSuiWalletInfo( MixinNativeNearWalletInfo( MixinNativeThorchainWalletInfo( @@ -166,6 +168,7 @@ class NativeHDWalletInfo core.StarknetWalletInfo, core.TronWalletInfo, core.TonWalletInfo, + core.AptosWalletInfo, core.SuiWalletInfo, core.NearWalletInfo, core.ThorchainWalletInfo, @@ -232,6 +235,8 @@ class NativeHDWalletInfo return core.tronDescribePath(msg.path) case 'ton': return core.tonDescribePath(msg.path) + case 'aptos': + return core.aptosDescribePath(msg.path) default: throw new Error('Unsupported path') } @@ -246,6 +251,7 @@ export class NativeHDWallet MixinNativeStarknetWallet( MixinNativeTronWallet( MixinNativeTonWallet( + MixinNativeAptosWallet( MixinNativeSuiWallet( MixinNativeNearWallet( MixinNativeThorchainWallet( @@ -277,6 +283,7 @@ export class NativeHDWallet core.StarknetWallet, core.TronWallet, core.TonWallet, + core.AptosWallet, core.SuiWallet, core.NearWallet, core.ThorchainWallet, @@ -453,6 +460,7 @@ export class NativeHDWallet super.solanaInitializeWallet(ed25519MasterKey), super.suiInitializeWallet(ed25519MasterKey), super.nearInitializeWallet(ed25519MasterKey), + super.aptosInitializeWallet(ed25519MasterKey), ] if (this.#tonMasterKey) { @@ -505,6 +513,7 @@ export class NativeHDWallet super.suiWipe() super.nearWipe() super.tonWipe() + super.aptosWipe() super.btcWipe() super.ethWipe() super.cosmosWipe() From 31978aa3b3800c26543711a9bd7eb075b8a7697e Mon Sep 17 00:00:00 2001 From: Discostu Date: Thu, 14 May 2026 22:50:15 +0200 Subject: [PATCH 02/16] feat: add Aptos CAIP constants, types, and utils (web-ohh.4) --- packages/caip/src/constants.ts | 9 +++++ packages/chain-adapters/src/aptos/index.ts | 1 + packages/chain-adapters/src/aptos/types.ts | 33 +++++++++++++++++++ packages/chain-adapters/src/index.ts | 1 + packages/chain-adapters/src/types.ts | 8 +++++ packages/types/src/base.ts | 3 ++ packages/utils/src/assetData/baseAssets.ts | 16 +++++++++ packages/utils/src/assetData/getBaseAsset.ts | 3 ++ packages/utils/src/chainIdToFeeAssetId.ts | 3 ++ .../utils/src/getAssetNamespaceFromChainId.ts | 2 ++ packages/utils/src/getChainShortName.ts | 2 ++ 11 files changed, 81 insertions(+) create mode 100644 packages/chain-adapters/src/aptos/index.ts create mode 100644 packages/chain-adapters/src/aptos/types.ts diff --git a/packages/caip/src/constants.ts b/packages/caip/src/constants.ts index d8c5f9f0e3a..417fc05edb7 100644 --- a/packages/caip/src/constants.ts +++ b/packages/caip/src/constants.ts @@ -50,6 +50,7 @@ export const suiAssetId: AssetId = 'sui:35834a8a/slip44:784' export const nearAssetId: AssetId = 'near:mainnet/slip44:397' export const starknetAssetId: AssetId = 'starknet:SN_MAIN/slip44:9004' export const tonAssetId: AssetId = 'ton:mainnet/slip44:607' +export const aptosAssetId: AssetId = 'aptos:861fb8e6/slip44:637' export const uniV2EthFoxArbitrumAssetId: AssetId = 'eip155:42161/erc20:0x5f6ce0ca13b87bd738519545d3e018e70e339c24' @@ -143,6 +144,7 @@ export const suiChainId: ChainId = 'sui:35834a8a' export const nearChainId: ChainId = 'near:mainnet' export const starknetChainId: ChainId = 'starknet:SN_MAIN' export const tonChainId: ChainId = 'ton:mainnet' +export const aptosChainId: ChainId = 'aptos:861fb8e6' export const CHAIN_NAMESPACE = { Evm: 'eip155', @@ -154,6 +156,7 @@ export const CHAIN_NAMESPACE = { Near: 'near', Starknet: 'starknet', Ton: 'ton', + Aptos: 'aptos', } as const type ValidChainMap = { @@ -211,6 +214,7 @@ export const CHAIN_REFERENCE = { StarknetMainnet: 'SN_MAIN', // https://namespaces.chainagnostic.org/starknet/caip2 TonMainnet: 'mainnet', // TON Mainnet AbstractMainnet: '2741', // https://abscan.org + AptosMainnet: '861fb8e6', // First 8 chars of Aptos mainnet genesis hash } as const export const ASSET_NAMESPACE = { @@ -225,6 +229,7 @@ export const ASSET_NAMESPACE = { nep141: 'nep141', // NEAR fungible token standard: https://nomicon.io/Standards/Tokens/FungibleToken/Core starknetToken: 'token', jetton: 'jetton', // TON fungible token standard (TEP-74) + aptosCoin: 'coin', // Aptos native coin type } as const export const ASSET_REFERENCE = { @@ -278,6 +283,7 @@ export const ASSET_REFERENCE = { Starknet: '9004', Ton: '607', Abstract: '60', // evm chain which uses ethereum derivation path as common practice + Aptos: '637', } as const export const VALID_CHAIN_IDS: ValidChainMap = Object.freeze({ @@ -337,6 +343,7 @@ export const VALID_CHAIN_IDS: ValidChainMap = Object.freeze({ [CHAIN_NAMESPACE.Near]: [CHAIN_REFERENCE.NearMainnet], [CHAIN_NAMESPACE.Starknet]: [CHAIN_REFERENCE.StarknetMainnet], [CHAIN_NAMESPACE.Ton]: [CHAIN_REFERENCE.TonMainnet], + [CHAIN_NAMESPACE.Aptos]: [CHAIN_REFERENCE.AptosMainnet], }) type ValidAssetNamespace = { @@ -358,6 +365,7 @@ export const VALID_ASSET_NAMESPACE: ValidAssetNamespace = Object.freeze({ [CHAIN_NAMESPACE.Near]: [ASSET_NAMESPACE.slip44, ASSET_NAMESPACE.nep141], [CHAIN_NAMESPACE.Starknet]: [ASSET_NAMESPACE.slip44, ASSET_NAMESPACE.starknetToken], [CHAIN_NAMESPACE.Ton]: [ASSET_NAMESPACE.slip44, ASSET_NAMESPACE.jetton], + [CHAIN_NAMESPACE.Aptos]: [ASSET_NAMESPACE.slip44, ASSET_NAMESPACE.aptosCoin], }) // We should prob change this once we add more chains @@ -408,4 +416,5 @@ export const FEE_ASSET_IDS = [ katanaAssetId, etherealAssetId, flowEvmAssetId, + aptosAssetId, ] diff --git a/packages/chain-adapters/src/aptos/index.ts b/packages/chain-adapters/src/aptos/index.ts new file mode 100644 index 00000000000..c9f6f047dc0 --- /dev/null +++ b/packages/chain-adapters/src/aptos/index.ts @@ -0,0 +1 @@ +export * from './types' diff --git a/packages/chain-adapters/src/aptos/types.ts b/packages/chain-adapters/src/aptos/types.ts new file mode 100644 index 00000000000..15f43520c29 --- /dev/null +++ b/packages/chain-adapters/src/aptos/types.ts @@ -0,0 +1,33 @@ +import type { AssetId } from '@shapeshiftoss/caip' + +import type * as types from '../types' + +export type AptosToken = types.AssetBalance & { + assetId: AssetId + symbol: string + name: string + precision: number +} + +export type AptosAccount = { + tokens?: AptosToken[] +} + +export type BuildTxInput = { + memo?: string +} + +export type AptosGetFeeDataInput = { + from: string + memo?: string +} + +export type AptosFeeData = { + gasEstimate: string + gasUnitPrice: string + maxGasAmount: string +} + +export type Account = AptosAccount +export type FeeData = AptosFeeData +export type GetFeeDataInput = AptosGetFeeDataInput diff --git a/packages/chain-adapters/src/index.ts b/packages/chain-adapters/src/index.ts index ba39e5f0f74..1e5594bebba 100644 --- a/packages/chain-adapters/src/index.ts +++ b/packages/chain-adapters/src/index.ts @@ -8,6 +8,7 @@ export * from './evm' export * as solana from './solana' export * as starknet from './starknet' export * as sui from './sui' +export * as aptos from './aptos' export * as ton from './ton' export * as tron from './tron' export * as near from './near' diff --git a/packages/chain-adapters/src/types.ts b/packages/chain-adapters/src/types.ts index fbdd05171fb..3b2692b39c8 100644 --- a/packages/chain-adapters/src/types.ts +++ b/packages/chain-adapters/src/types.ts @@ -1,5 +1,6 @@ import type { ChainId, Nominal } from '@shapeshiftoss/caip' import type { + AptosSignTx, BTCSignTx, CosmosSignTx, ETHSignTx, @@ -25,6 +26,7 @@ import type * as near from './near/types' import type * as solana from './solana/types' import type * as starknet from './starknet/types' import type * as sui from './sui/types' +import type * as aptos from './aptos/types' import type * as ton from './ton/types' import type * as tron from './tron/types' import type * as utxo from './utxo/types' @@ -85,6 +87,7 @@ type ChainSpecificAccount = ChainSpecific< [KnownChainIds.NearMainnet]: near.Account [KnownChainIds.StarknetMainnet]: starknet.Account [KnownChainIds.TonMainnet]: ton.Account + [KnownChainIds.AptosMainnet]: aptos.Account } > @@ -159,6 +162,7 @@ type ChainSpecificFeeData = ChainSpecific< [KnownChainIds.NearMainnet]: near.FeeData [KnownChainIds.StarknetMainnet]: starknet.FeeData [KnownChainIds.TonMainnet]: ton.FeeData + [KnownChainIds.AptosMainnet]: aptos.FeeData } > @@ -266,6 +270,7 @@ export type ChainSignTx = { [KnownChainIds.NearMainnet]: near.NearSignTx [KnownChainIds.StarknetMainnet]: StarknetSignTx [KnownChainIds.TonMainnet]: ton.TonSignTx + [KnownChainIds.AptosMainnet]: AptosSignTx } export type SignTx = T extends keyof ChainSignTx ? ChainSignTx[T] : never @@ -345,6 +350,7 @@ export type ChainSpecificBuildTxData = ChainSpecific< [KnownChainIds.NearMainnet]: near.BuildTxInput [KnownChainIds.StarknetMainnet]: starknet.BuildTxInput [KnownChainIds.TonMainnet]: ton.BuildTxInput + [KnownChainIds.AptosMainnet]: aptos.BuildTxInput } > @@ -471,6 +477,7 @@ type ChainSpecificGetFeeDataInput = ChainSpecific< [KnownChainIds.NearMainnet]: near.GetFeeDataInput [KnownChainIds.StarknetMainnet]: starknet.GetFeeDataInput [KnownChainIds.TonMainnet]: ton.GetFeeDataInput + [KnownChainIds.AptosMainnet]: aptos.GetFeeDataInput } > export type GetFeeDataInput = { @@ -566,6 +573,7 @@ export enum ChainAdapterDisplayName { Starknet = 'Starknet', Ton = 'TON', Abstract = 'Abstract', + Aptos = 'Aptos', } export type BroadcastTransactionInput = { diff --git a/packages/types/src/base.ts b/packages/types/src/base.ts index eeeb6313fb3..6eabc210d13 100644 --- a/packages/types/src/base.ts +++ b/packages/types/src/base.ts @@ -63,6 +63,7 @@ export enum KnownChainIds { StarknetMainnet = 'starknet:SN_MAIN', TonMainnet = 'ton:mainnet', AbstractMainnet = 'eip155:2741', + AptosMainnet = 'aptos:861fb8e6', } export type EvmChainId = @@ -128,6 +129,8 @@ export type StarknetChainId = KnownChainIds.StarknetMainnet export type TonChainId = KnownChainIds.TonMainnet +export type AptosChainId = KnownChainIds.AptosMainnet + export enum WithdrawType { DELAYED, INSTANT, diff --git a/packages/utils/src/assetData/baseAssets.ts b/packages/utils/src/assetData/baseAssets.ts index 2531ebc9c83..eb3b022989a 100644 --- a/packages/utils/src/assetData/baseAssets.ts +++ b/packages/utils/src/assetData/baseAssets.ts @@ -857,3 +857,19 @@ export const ton: Readonly = Object.freeze({ explorerTxLink: 'https://tonscan.org/tx/', relatedAssetKey: null, }) + +export const aptos: Readonly = Object.freeze({ + assetId: caip.aptosAssetId, + chainId: caip.aptosChainId, + name: 'Aptos', + networkName: 'Aptos', + symbol: 'APT', + precision: 8, + color: '#2CD5E5', + networkColor: '#2CD5E5', + icon: 'https://assets.coingecko.com/coins/images/25788/large/aptos.png?1696520069', + explorer: 'https://explorer.aptoslabs.com', + explorerAddressLink: 'https://explorer.aptoslabs.com/account/', + explorerTxLink: 'https://explorer.aptoslabs.com/txn/', + relatedAssetKey: null, +}) diff --git a/packages/utils/src/assetData/getBaseAsset.ts b/packages/utils/src/assetData/getBaseAsset.ts index 33e613002c7..d4ef05109be 100644 --- a/packages/utils/src/assetData/getBaseAsset.ts +++ b/packages/utils/src/assetData/getBaseAsset.ts @@ -4,6 +4,7 @@ import { KnownChainIds } from '@shapeshiftoss/types' import { assertUnreachable } from '../assertUnreachable' import { + aptos, abstract, arbitrum, atom, @@ -156,6 +157,8 @@ export const getBaseAsset = (chainId: ChainId): Readonly => { return ton case KnownChainIds.AbstractMainnet: return abstract + case KnownChainIds.AptosMainnet: + return aptos default: return assertUnreachable(knownChainId) } diff --git a/packages/utils/src/chainIdToFeeAssetId.ts b/packages/utils/src/chainIdToFeeAssetId.ts index 01a9bfab4bb..8fba94b0102 100644 --- a/packages/utils/src/chainIdToFeeAssetId.ts +++ b/packages/utils/src/chainIdToFeeAssetId.ts @@ -1,5 +1,6 @@ import type { AssetId, ChainId } from '@shapeshiftoss/caip' import { + aptosAssetId, abstractAssetId, arbitrumAssetId, avalancheAssetId, @@ -155,6 +156,8 @@ export const chainIdToFeeAssetId = (_chainId: ChainId): AssetId => { return tonAssetId case KnownChainIds.AbstractMainnet: return abstractAssetId + case KnownChainIds.AptosMainnet: + return aptosAssetId default: return assertUnreachable(chainId) } diff --git a/packages/utils/src/getAssetNamespaceFromChainId.ts b/packages/utils/src/getAssetNamespaceFromChainId.ts index 107b891cb36..319ddbdab5e 100644 --- a/packages/utils/src/getAssetNamespaceFromChainId.ts +++ b/packages/utils/src/getAssetNamespaceFromChainId.ts @@ -54,6 +54,8 @@ export const getAssetNamespaceFromChainId = (chainId: KnownChainIds): AssetNames return ASSET_NAMESPACE.starknetToken case KnownChainIds.TonMainnet: return ASSET_NAMESPACE.jetton + case KnownChainIds.AptosMainnet: + return ASSET_NAMESPACE.aptosCoin case KnownChainIds.CosmosMainnet: case KnownChainIds.BitcoinMainnet: case KnownChainIds.BitcoinCashMainnet: diff --git a/packages/utils/src/getChainShortName.ts b/packages/utils/src/getChainShortName.ts index a5012b04e34..e327ab5fea2 100644 --- a/packages/utils/src/getChainShortName.ts +++ b/packages/utils/src/getChainShortName.ts @@ -102,6 +102,8 @@ export const getChainShortName = (chainId: KnownChainIds) => { return 'TON' case KnownChainIds.AbstractMainnet: return 'ABS' + case KnownChainIds.AptosMainnet: + return 'APT' default: { assertUnreachable(chainId) } From d8bb82ec16f9e383d9145433ecfcdbf0cb7c6794 Mon Sep 17 00:00:00 2001 From: Discostu Date: Thu, 14 May 2026 22:56:40 +0200 Subject: [PATCH 03/16] feat: add AptosChainAdapter implementation (web-ohh.3) --- .../src/aptos/AptosChainAdapter.ts | 489 ++++++++++++++++++ packages/chain-adapters/src/aptos/index.ts | 2 + 2 files changed, 491 insertions(+) create mode 100644 packages/chain-adapters/src/aptos/AptosChainAdapter.ts diff --git a/packages/chain-adapters/src/aptos/AptosChainAdapter.ts b/packages/chain-adapters/src/aptos/AptosChainAdapter.ts new file mode 100644 index 00000000000..d1dfec4f21d --- /dev/null +++ b/packages/chain-adapters/src/aptos/AptosChainAdapter.ts @@ -0,0 +1,489 @@ +import type { AssetId, ChainId } from '@shapeshiftoss/caip' +import { + ASSET_REFERENCE, + aptosAssetId, + aptosChainId, +} from '@shapeshiftoss/caip' +import type { AptosSignTx, AptosWallet, HDWallet } from '@shapeshiftoss/hdwallet-core' +import { supportsAptos } from '@shapeshiftoss/hdwallet-core' +import type { Bip44Params, RootBip44Params } from '@shapeshiftoss/types' +import { KnownChainIds } from '@shapeshiftoss/types' +import { TransferType, TxStatus } from '@shapeshiftoss/unchained-client' + +import type { ChainAdapter as IChainAdapter } from '../api' +import { ChainAdapterError, ErrorHandler } from '../error/ErrorHandler' +import type { + Account, + BroadcastTransactionInput, + BuildSendApiTxInput, + BuildSendTxInput, + FeeDataEstimate, + GetAddressInput, + GetBip44ParamsInput, + GetFeeDataInput, + SignAndBroadcastTransactionInput, + SignTx, + SignTxInput, + Transaction, + ValidAddressResult, +} from '../types' +import { ChainAdapterDisplayName, ValidAddressResultType } from '../types' +import { toAddressNList, verifyLedgerAppOpen } from '../utils' + +export interface ChainAdapterArgs { + rpcUrl: string +} + +export class ChainAdapter implements IChainAdapter { + static readonly rootBip44Params: RootBip44Params = { + purpose: 44, + coinType: Number(ASSET_REFERENCE.Aptos), + accountNumber: 0, + } + + protected readonly chainId = aptosChainId + protected readonly assetId = aptosAssetId + protected readonly rpcUrl: string + + constructor(args: ChainAdapterArgs) { + this.rpcUrl = args.rpcUrl + } + + private assertSupportsChain(wallet: HDWallet): asserts wallet is AptosWallet { + if (!supportsAptos(wallet)) { + throw new ChainAdapterError(`wallet does not support: ${this.getDisplayName()}`, { + translation: 'chainAdapters.errors.unsupportedChain', + options: { chain: this.getDisplayName() }, + }) + } + } + + getName() { + return 'Aptos' + } + + getDisplayName() { + return ChainAdapterDisplayName.Aptos + } + + getType(): KnownChainIds.AptosMainnet { + return KnownChainIds.AptosMainnet + } + + getFeeAssetId(): AssetId { + return this.assetId + } + + getChainId(): ChainId { + return this.chainId + } + + getBip44Params({ accountNumber }: GetBip44ParamsInput): Bip44Params { + if (accountNumber < 0) throw new Error('accountNumber must be >= 0') + return { + ...ChainAdapter.rootBip44Params, + accountNumber, + isChange: false, + addressIndex: 0, + } + } + + async getAddress(input: GetAddressInput): Promise { + try { + const { accountNumber, pubKey, wallet, showOnDevice = false } = input + + if (pubKey) return pubKey + + if (!wallet) throw new Error('wallet is required') + this.assertSupportsChain(wallet) + + await verifyLedgerAppOpen(this.chainId, wallet) + + const address = await wallet.aptosGetAddress({ + addressNList: toAddressNList(this.getBip44Params({ accountNumber })), + showDisplay: showOnDevice, + }) + + if (!address) throw new Error('error getting address from wallet') + + return address + } catch (err) { + return ErrorHandler(err, { + translation: 'chainAdapters.errors.getAddress', + }) + } + } + + async getAccount(pubkey: string): Promise> { + try { + const response = await fetch(`${this.rpcUrl}/accounts/${pubkey}`) + + if (!response.ok) { + throw new Error(`Aptos account request failed: ${response.status}`) + } + + const accountData = await response.json() + + // Aptos returns coin balances as an array of { coin: { type: string }, coin?: { value: string } } + // The native APT balance is under 0x1::aptos_coin::AptosCoin + let balance = '0' + + if (Array.isArray(accountData)) { + // Some endpoints return array of coin resources + for (const resource of accountData) { + if (resource.type === '0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>') { + balance = resource.data?.coin?.value ?? '0' + break + } + } + } + + return { + balance, + chainId: this.chainId, + assetId: this.assetId, + chain: this.getType(), + chainSpecific: {}, + pubkey, + } + } catch (err) { + return ErrorHandler(err, { + translation: 'chainAdapters.errors.getAccount', + options: { pubkey }, + }) + } + } + + validateAddress(address: string): Promise { + try { + if (!address.startsWith('0x')) { + return Promise.resolve({ valid: false, result: ValidAddressResultType.Invalid }) + } + + const hexPart = address.slice(2) + if (hexPart.length !== 64) { + return Promise.resolve({ valid: false, result: ValidAddressResultType.Invalid }) + } + + if (!/^[0-9a-fA-F]{64}$/.test(hexPart)) { + return Promise.resolve({ valid: false, result: ValidAddressResultType.Invalid }) + } + + return Promise.resolve({ valid: true, result: ValidAddressResultType.Valid }) + } catch { + return Promise.resolve({ valid: false, result: ValidAddressResultType.Invalid }) + } + } + + getTxHistory(): Promise { + throw new Error('Aptos transaction history not yet implemented') + } + + async buildSendApiTransaction( + input: BuildSendApiTxInput, + ): Promise> { + try { + const { from, accountNumber, to, value } = input + + // Build a raw Aptos transaction for signing + // We construct the BCS-encoded transaction payload for 0x1::coin::transfer + const sequenceNumber = await this.getSequenceNumber(from) + const gasEstimate = await this.getGasPrice() + const chainId = 1 // Aptos mainnet chain ID + + // Build the raw transaction structure + const txData = { + sender: from, + sequence_number: sequenceNumber, + max_gas_amount: '2000', + gas_unit_price: gasEstimate, + expiration_timestamp_secs: String(Math.floor(Date.now() / 1000) + 3600), + chain_id: chainId, + payload: { + type: 'entry_function_payload', + function: '0x1::coin::transfer', + type_arguments: ['0x1::aptos_coin::AptosCoin'], + arguments: [to, value], + }, + } + + // Serialize the transaction for the hardware wallet to sign + // The wallet expects raw BCS bytes via aptosSignTx + const txBytes = new TextEncoder().encode(JSON.stringify(txData)) + + return { + addressNList: toAddressNList(this.getBip44Params({ accountNumber })), + txBytes, + } + } catch (err) { + return ErrorHandler(err, { + translation: 'chainAdapters.errors.buildTransaction', + }) + } + } + + async buildSendTransaction(input: BuildSendTxInput): Promise<{ + txToSign: SignTx + }> { + try { + const from = await this.getAddress(input) + const txToSign = await this.buildSendApiTransaction({ ...input, from }) + + return { txToSign: { ...txToSign, ...(input.pubKey ? { pubKey: input.pubKey } : {}) } } + } catch (err) { + return ErrorHandler(err, { + translation: 'chainAdapters.errors.buildTransaction', + }) + } + } + + async signTransaction( + signTxInput: SignTxInput>, + ): Promise { + try { + const { txToSign, wallet } = signTxInput + + if (!wallet) throw new Error('wallet is required') + this.assertSupportsChain(wallet) + + await verifyLedgerAppOpen(this.chainId, wallet) + + const signedTx = await wallet.aptosSignTx({ + addressNList: txToSign.addressNList, + txBytes: txToSign.txBytes, + }) + + if (!signedTx?.signature || !signedTx?.publicKey) { + throw new Error('error signing tx - missing signature or publicKey') + } + + return JSON.stringify({ + signature: signedTx.signature, + publicKey: signedTx.publicKey, + txBytes: Array.from(signedTx.txBytes), + }) + } catch (err) { + return ErrorHandler(err, { + translation: 'chainAdapters.errors.signTransaction', + }) + } + } + + async signAndBroadcastTransaction({ + signTxInput, + }: SignAndBroadcastTransactionInput): Promise { + try { + const signedTxHex = await this.signTransaction(signTxInput) + return this.broadcastTransaction({ hex: signedTxHex }) + } catch (err) { + return ErrorHandler(err, { + translation: 'chainAdapters.errors.signAndBroadcastTransaction', + }) + } + } + + async broadcastTransaction(input: BroadcastTransactionInput): Promise { + try { + const { hex } = input + const parsed = JSON.parse(hex) + + // Submit the signed transaction to the Aptos REST API + // The signed transaction needs to be submitted as a BCS-encoded body + const response = await fetch(`${this.rpcUrl}/transactions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sender: `0x${parsed.publicKey}`, + signature: { + type: 'ed25519_signature', + public_key: `0x${parsed.publicKey}`, + signature: `0x${parsed.signature}`, + }, + payload: JSON.parse(new TextDecoder().decode(new Uint8Array(parsed.txBytes))).payload, + }), + }) + + if (!response.ok) { + const errorBody = await response.text() + throw new Error(`Aptos broadcast failed: ${response.status} - ${errorBody}`) + } + + const result = await response.json() + return result.hash ?? result.version?.toString() ?? '' + } catch (err) { + return ErrorHandler(err, { + translation: 'chainAdapters.errors.broadcastTransaction', + }) + } + } + + async getFeeData( + input: GetFeeDataInput, + ): Promise> { + try { + const gasPrice = await this.getGasPrice() + const maxGasAmount = '2000' + const txFee = (BigInt(maxGasAmount) * BigInt(gasPrice)).toString() + + const feeData = { + gasEstimate: txFee, + gasUnitPrice: gasPrice, + maxGasAmount, + } + + return { + fast: { txFee, chainSpecific: feeData }, + average: { txFee, chainSpecific: feeData }, + slow: { txFee, chainSpecific: feeData }, + } + } catch (err) { + return ErrorHandler(err, { + translation: 'chainAdapters.errors.getFeeData', + }) + } + } + + subscribeTxs(): Promise { + return Promise.resolve() + } + + unsubscribeTxs(): void { + return + } + + closeTxs(): void { + return + } + + async parseTx(txHashOrTx: unknown, pubkey: string): Promise { + try { + const txHash = typeof txHashOrTx === 'string' ? txHashOrTx : '' + + const response = await fetch(`${this.rpcUrl}/transactions/by_hash/${txHash}`) + if (!response.ok) { + throw new Error(`Aptos tx lookup failed: ${response.status}`) + } + + const tx = await response.json() + + const txid = tx.hash ?? txHash + const blockHeight = Number(tx.version ?? 0) + const blockTime = tx.timestamp ? Math.floor(Number(tx.timestamp) / 1000) : 0 + + const success = tx.success !== false + const status = success ? TxStatus.Confirmed : TxStatus.Failed + + const gasUsed = tx.gas_used ?? '0' + const gasUnitPrice = tx.gas_unit_price ?? '0' + const fee = { + assetId: this.assetId, + value: (BigInt(gasUsed) * BigInt(gasUnitPrice)).toString(), + } + + const transfers: Transaction['transfers'] = [] + + // Parse events for transfers + const events = tx.events ?? [] + for (const event of events) { + if (event.type === '0x1::coin::WithdrawEvent' || event.type === '0x1::coin::DepositEvent') { + // These are coin module events, skip in favor of ChangeEvent + continue + } + + if (event.type?.includes('::CoinTransfer') || event.type?.includes('::Withdraw') || event.type?.includes('::Deposit')) { + const amount = event.data?.amount ?? event.data?.value ?? '0' + const isFromSender = event.guid?.account_address === pubkey || event.data?.from === pubkey + const isToSender = event.data?.to === pubkey + + if (isFromSender) { + transfers.push({ + assetId: this.assetId, + from: [event.data?.from ?? event.guid?.account_address ?? pubkey], + to: [event.data?.to ?? ''], + type: TransferType.Send, + value: amount, + }) + } + if (isToSender) { + transfers.push({ + assetId: this.assetId, + from: [event.data?.from ?? ''], + to: [event.data?.to ?? pubkey], + type: TransferType.Receive, + value: amount, + }) + } + } + } + + // If no transfers found from events, try to parse from the payload + if (transfers.length === 0 && tx.payload?.function === '0x1::coin::transfer') { + const args = tx.payload.arguments ?? [] + const recipient = args[0] ?? '' + const amount = args[1] ?? '0' + const sender = tx.sender ?? pubkey + + const isSend = sender === pubkey + const isReceive = recipient === pubkey + + if (isSend) { + transfers.push({ + assetId: this.assetId, + from: [sender], + to: [recipient], + type: TransferType.Send, + value: String(amount), + }) + } + if (isReceive) { + transfers.push({ + assetId: this.assetId, + from: [sender], + to: [recipient], + type: TransferType.Receive, + value: String(amount), + }) + } + } + + return { + txid, + blockHeight, + blockTime, + blockHash: undefined, + chainId: this.chainId, + confirmations: status === TxStatus.Confirmed ? 1 : 0, + status, + fee, + transfers, + pubkey, + } + } catch (err) { + return ErrorHandler(err, { + translation: 'chainAdapters.errors.parseTx', + }) + } + } + + private async getSequenceNumber(address: string): Promise { + try { + const response = await fetch(`${this.rpcUrl}/accounts/${address}`) + if (!response.ok) return '0' + const data = await response.json() + return data.sequence_number ?? '0' + } catch { + return '0' + } + } + + private async getGasPrice(): Promise { + try { + const response = await fetch(`${this.rpcUrl}/transactions/estimate_gas_price`) + if (!response.ok) return '100' + const data = await response.json() + return data.gas_estimate?.toString() ?? '100' + } catch { + return '100' + } + } +} diff --git a/packages/chain-adapters/src/aptos/index.ts b/packages/chain-adapters/src/aptos/index.ts index c9f6f047dc0..b63c7b8b23f 100644 --- a/packages/chain-adapters/src/aptos/index.ts +++ b/packages/chain-adapters/src/aptos/index.ts @@ -1 +1,3 @@ +export { ChainAdapter } from './AptosChainAdapter' + export * from './types' From b36e18f4512f47c3ded54bc03cbf7cc566daedea Mon Sep 17 00:00:00 2001 From: Discostu Date: Thu, 14 May 2026 22:57:59 +0200 Subject: [PATCH 04/16] feat: add Aptos plugin, feature flag, config, and CSP headers (web-ohh.5) --- .env | 2 ++ .env.development | 2 ++ headers/csps/chains/aptos.ts | 13 ++++++++ headers/csps/index.ts | 2 ++ src/config.ts | 2 ++ src/plugins/aptos/index.tsx | 30 +++++++++++++++++++ .../preferencesSlice/preferencesSlice.ts | 2 ++ src/test/mocks/store.ts | 1 + src/vite-env.d.ts | 2 ++ 9 files changed, 56 insertions(+) create mode 100644 headers/csps/chains/aptos.ts create mode 100644 src/plugins/aptos/index.tsx diff --git a/.env b/.env index 4dd4f90ced3..67920560f3e 100644 --- a/.env +++ b/.env @@ -120,6 +120,8 @@ VITE_FEATURE_BOB_GATEWAY_SWAP=true VITE_FEATURE_USERBACK=true VITE_FEATURE_AGENTIC_CHAT=false VITE_FEATURE_MM_NATIVE_MULTICHAIN=false +VITE_FEATURE_APTOS=false +VITE_APTOS_NODE_URL=https://fullnode.mainnet.aptoslabs.com/v1 # experimental feature flags VITE_EXPERIMENTAL_CUSTOM_SEND_NONCE=false diff --git a/.env.development b/.env.development index 9f5bd818cf2..68b71b87427 100644 --- a/.env.development +++ b/.env.development @@ -38,6 +38,8 @@ VITE_FEATURE_YIELD_MULTI_ACCOUNT=true VITE_FEATURE_AGENTIC_CHAT=true VITE_FEATURE_MM_NATIVE_MULTICHAIN=true VITE_FEATURE_NOTIFICATIONS_WEBSERVICES=true +VITE_FEATURE_APTOS=true +VITE_APTOS_NODE_URL=https://fullnode.mainnet.aptoslabs.com/v1 # mixpanel VITE_MIXPANEL_TOKEN=a867ce40912a6b7d01d088cf62b0e1ff diff --git a/headers/csps/chains/aptos.ts b/headers/csps/chains/aptos.ts new file mode 100644 index 00000000000..d3879543557 --- /dev/null +++ b/headers/csps/chains/aptos.ts @@ -0,0 +1,13 @@ +import { loadEnv } from 'vite' + +import type { Csp } from '../../types' + +const mode = process.env.MODE ?? process.env.NODE_ENV ?? 'development' +const env = loadEnv(mode, process.cwd(), '') + +export const csp: Csp = { + 'connect-src': [ + env.VITE_APTOS_NODE_URL, + 'https://fullnode.mainnet.aptoslabs.com/', + ], +} diff --git a/headers/csps/index.ts b/headers/csps/index.ts index 7213d29df57..6407791749b 100644 --- a/headers/csps/index.ts +++ b/headers/csps/index.ts @@ -4,6 +4,7 @@ import { csp as trustwallet } from './assetService/trustwallet' import { csp as base } from './base' import { csp as chainflip } from './chainflip' import { csp as abstract } from './chains/abstract' +import { csp as aptos } from './chains/aptos' import { csp as arbitrum } from './chains/arbitrum' import { csp as avalanche } from './chains/avalanche' import { csp as baseChain } from './chains/base' @@ -131,6 +132,7 @@ export const csps = [ bitcoincash, blast, abstract, + aptos, bnbsmartchain, cosmos, dogecoin, diff --git a/src/config.ts b/src/config.ts index 7ce3704f299..7be93398b97 100644 --- a/src/config.ts +++ b/src/config.ts @@ -306,6 +306,8 @@ const validators = { VITE_FEATURE_PERFORMANCE_PROFILER: bool({ default: false }), VITE_FEATURE_AGENTIC_CHAT: bool({ default: false }), VITE_FEATURE_MM_NATIVE_MULTICHAIN: bool({ default: false }), + VITE_FEATURE_APTOS: bool({ default: false }), + VITE_APTOS_NODE_URL: url(), VITE_AGENTIC_SERVER_BASE_URL: url({ default: 'https://api.agent.shapeshift.com', }), diff --git a/src/plugins/aptos/index.tsx b/src/plugins/aptos/index.tsx new file mode 100644 index 00000000000..0cdcd3ea4ef --- /dev/null +++ b/src/plugins/aptos/index.tsx @@ -0,0 +1,30 @@ +import { aptos } from '@shapeshiftoss/chain-adapters' +import { KnownChainIds } from '@shapeshiftoss/types' + +import { getConfig } from '@/config' +import type { Plugins } from '@/plugins/types' + +// eslint-disable-next-line import/no-default-export +export default function register(): Plugins { + return [ + [ + 'aptosChainAdapter', + { + name: 'aptosChainAdapter', + featureFlag: ['Aptos'], + providers: { + chainAdapters: [ + [ + KnownChainIds.AptosMainnet, + () => { + return new aptos.ChainAdapter({ + rpcUrl: getConfig().VITE_APTOS_NODE_URL, + }) + }, + ], + ], + }, + }, + ], + ] +} diff --git a/src/state/slices/preferencesSlice/preferencesSlice.ts b/src/state/slices/preferencesSlice/preferencesSlice.ts index fdd2c2899ab..d5352eedcaf 100644 --- a/src/state/slices/preferencesSlice/preferencesSlice.ts +++ b/src/state/slices/preferencesSlice/preferencesSlice.ts @@ -140,6 +140,7 @@ export type FeatureFlags = { YieldMultiAccount: boolean EarnTab: boolean MmNativeMultichain: boolean + Aptos: boolean } export type Flag = keyof FeatureFlags @@ -314,6 +315,7 @@ const initialState: Preferences = { YieldMultiAccount: getConfig().VITE_FEATURE_YIELD_MULTI_ACCOUNT, EarnTab: getConfig().VITE_FEATURE_EARN_TAB, MmNativeMultichain: getConfig().VITE_FEATURE_MM_NATIVE_MULTICHAIN, + Aptos: getConfig().VITE_FEATURE_APTOS, }, selectedLocale: simpleLocale(), hasWalletSeenTcyClaimAlert: {}, diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index 783e0d0ee34..28cba6096fc 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -212,6 +212,7 @@ export const mockStore: ReduxState = { EarnTab: false, AgenticChat: false, MmNativeMultichain: false, + Aptos: true, }, showTopAssetsCarousel: true, quickBuyAmounts: [10, 50, 100], diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 6d9e156f17c..d50cd88effd 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -222,6 +222,8 @@ interface ImportMetaEnv { readonly VITE_SEI_NODE_URL: string readonly VITE_FEATURE_SEI: string readonly VITE_FEATURE_NOTIFICATIONS_WEBSERVICES: string + readonly VITE_FEATURE_APTOS: string + readonly VITE_APTOS_NODE_URL: string // Only present in *some* envs readonly VITE_MIXPANEL_TOKEN?: string From 442da85b25e49bfa0389fe7828a6479a8236d0a0 Mon Sep 17 00:00:00 2001 From: Discostu Date: Thu, 14 May 2026 23:08:43 +0200 Subject: [PATCH 05/16] feat: add Aptos CoinGecko integration and asset generation (web-ohh.7) --- packages/caip/src/adapters/coingecko/index.ts | 13 +++++++++++++ packages/caip/src/adapters/coingecko/utils.ts | 17 +++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/packages/caip/src/adapters/coingecko/index.ts b/packages/caip/src/adapters/coingecko/index.ts index 59983dcca4e..369df00d525 100644 --- a/packages/caip/src/adapters/coingecko/index.ts +++ b/packages/caip/src/adapters/coingecko/index.ts @@ -44,6 +44,7 @@ import { starknetChainId, storyChainId, suiChainId, + aptosChainId, thorchainChainId, tonChainId, tronChainId, @@ -97,6 +98,7 @@ export enum CoingeckoAssetPlatform { Starknet = 'starknet', Tron = 'tron', Sui = 'sui', + Aptos = 'aptos-network', Ton = 'the-open-network', Near = 'near-protocol', Abstract = 'abstract', @@ -243,6 +245,15 @@ export const chainIdToCoingeckoAssetPlatform = (chainId: ChainId): string => { `chainNamespace ${chainNamespace}, chainReference ${chainReference} not supported.`, ) } + case CHAIN_NAMESPACE.Aptos: + switch (chainReference) { + case CHAIN_REFERENCE.AptosMainnet: + return CoingeckoAssetPlatform.Aptos + default: + throw new Error( + `chainNamespace ${chainNamespace}, chainReference ${chainReference} not supported.`, + ) + } case CHAIN_NAMESPACE.Near: switch (chainReference) { case CHAIN_REFERENCE.NearMainnet: @@ -363,6 +374,8 @@ export const coingeckoAssetPlatformToChainId = ( return tronChainId case CoingeckoAssetPlatform.Sui: return suiChainId + case CoingeckoAssetPlatform.Aptos: + return aptosChainId case CoingeckoAssetPlatform.Ton: return tonChainId case CoingeckoAssetPlatform.Near: diff --git a/packages/caip/src/adapters/coingecko/utils.ts b/packages/caip/src/adapters/coingecko/utils.ts index 66a2846f900..b00b3548ff8 100644 --- a/packages/caip/src/adapters/coingecko/utils.ts +++ b/packages/caip/src/adapters/coingecko/utils.ts @@ -81,6 +81,8 @@ import { storyAssetId, storyChainId, suiAssetId, + aptosAssetId, + aptosChainId, suiChainId, thorchainChainId, tonAssetId, @@ -287,6 +289,20 @@ export const parseData = (coins: CoingeckoCoin[]): AssetMap => { } } + if (Object.keys(platforms).includes(CoingeckoAssetPlatform.Aptos)) { + try { + const assetId = toAssetId({ + chainNamespace: CHAIN_NAMESPACE.Aptos, + chainReference: CHAIN_REFERENCE.AptosMainnet, + assetNamespace: ASSET_NAMESPACE.aptosCoin, + assetReference: platforms[CoingeckoAssetPlatform.Aptos], + }) + prev[aptosChainId][assetId] = id + } catch { + // unable to create assetId, skip token + } + } + if (Object.keys(platforms).includes(CoingeckoAssetPlatform.Monad)) { try { const assetId = toAssetId({ @@ -707,6 +723,7 @@ export const parseData = (coins: CoingeckoCoin[]): AssetMap => { [suiChainId]: { [suiAssetId]: 'sui' }, [nearChainId]: { [nearAssetId]: 'near' }, [tonChainId]: { [tonAssetId]: 'the-open-network' }, + [aptosChainId]: { [aptosAssetId]: 'aptos' }, }, ) From 5b7eb18f7afe8ad8e1ac248dd640cbb101cdeb7a Mon Sep 17 00:00:00 2001 From: Discostu Date: Thu, 14 May 2026 23:09:37 +0200 Subject: [PATCH 06/16] feat: add Aptos account derivation and portfolio wiring (web-ohh.6) --- .../useSendActionSubscriber.tsx | 7 +++ .../useWalletSupportsChain.ts | 4 ++ src/lib/account/account.ts | 4 ++ src/lib/account/aptos.ts | 28 +++++++++++ src/lib/utils/aptos.ts | 45 ++++++++++++++++++ .../slices/portfolioSlice/utils/index.ts | 47 +++++++++++++++++++ 6 files changed, 135 insertions(+) create mode 100644 src/lib/account/aptos.ts create mode 100644 src/lib/utils/aptos.ts diff --git a/src/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsx b/src/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsx index d35367d8196..aa8be257b50 100644 --- a/src/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsx +++ b/src/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsx @@ -12,6 +12,7 @@ import { useActionCenterContext } from '@/components/Layout/Header/ActionCenter/ import { GenericTransactionNotification } from '@/components/Layout/Header/ActionCenter/components/Notifications/GenericTransactionNotification' import { SECOND_CLASS_CHAINS } from '@/constants/chains' import { getChainAdapterManager } from '@/context/PluginProvider/chainAdapterSingleton' +import { getAptosTransactionStatus } from '@/lib/utils/aptos' import { getNearTransactionStatus } from '@/lib/utils/near' import { getStarknetTransactionStatus, isStarknetChainAdapter } from '@/lib/utils/starknet' import { getSuiTransactionStatus } from '@/lib/utils/sui' @@ -211,6 +212,12 @@ export const useSendActionSubscriber = () => { suiTxStatus === TxStatus.Confirmed || suiTxStatus === TxStatus.Failed break } + case KnownChainIds.AptosMainnet: { + const aptosTxStatus = await getAptosTransactionStatus(txHash) + isConfirmed = + aptosTxStatus === TxStatus.Confirmed || aptosTxStatus === TxStatus.Failed + break + } case KnownChainIds.NearMainnet: { const nearTxStatus = await getNearTransactionStatus(txHash) isConfirmed = diff --git a/src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts b/src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts index 61b56d3114a..26cc90a0a17 100644 --- a/src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts +++ b/src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts @@ -43,6 +43,7 @@ import { storyChainId, suiChainId, thorchainChainId, + aptosChainId, tonChainId, tronChainId, unichainChainId, @@ -94,6 +95,7 @@ import { supportsSonic, supportsStarknet, supportsStory, + supportsAptos, supportsSui, supportsThorchain, supportsTron, @@ -321,6 +323,8 @@ export const walletSupportsChain = ({ return supportsTron(wallet) case suiChainId: return supportsSui(wallet) + case aptosChainId: + return supportsAptos(wallet) case nearChainId: return isNearEnabled && supportsNear(wallet) case starknetChainId: diff --git a/src/lib/account/account.ts b/src/lib/account/account.ts index b7a4af6d59b..7e695a14b68 100644 --- a/src/lib/account/account.ts +++ b/src/lib/account/account.ts @@ -1,9 +1,11 @@ import type { ChainId } from '@shapeshiftoss/caip' import { CHAIN_NAMESPACE, fromChainId } from '@shapeshiftoss/caip' +import type { ChainNamespace } from '@shapeshiftoss/caip' import type { HDWallet } from '@shapeshiftoss/hdwallet-core' import type { AccountMetadataById } from '@shapeshiftoss/types' import merge from 'lodash/merge' +import { deriveAptosAccountIdsAndMetadata } from './aptos' import { deriveCosmosSdkAccountIdsAndMetadata } from './cosmosSdk' import { deriveEvmAccountIdsAndMetadata } from './evm' import { deriveNearAccountIdsAndMetadata } from './near' @@ -26,6 +28,7 @@ export const deriveAccountIdsAndMetadataForChainNamespace = { [CHAIN_NAMESPACE.Near]: deriveNearAccountIdsAndMetadata, [CHAIN_NAMESPACE.Starknet]: deriveStarknetAccountIdsAndMetadata, [CHAIN_NAMESPACE.Ton]: deriveTonAccountIdsAndMetadata, + [CHAIN_NAMESPACE.Aptos]: deriveAptosAccountIdsAndMetadata, } as const export type DeriveAccountIdsAndMetadataArgs = { @@ -57,6 +60,7 @@ export const deriveAccountIdsAndMetadata: DeriveAccountIdsAndMetadata = async ar [CHAIN_NAMESPACE.Near]: [], [CHAIN_NAMESPACE.Starknet]: [], [CHAIN_NAMESPACE.Ton]: [], + [CHAIN_NAMESPACE.Aptos]: [], } const chainIdsByChainNamespace = chainIds.reduce((acc, chainId) => { const { chainNamespace } = fromChainId(chainId) diff --git a/src/lib/account/aptos.ts b/src/lib/account/aptos.ts new file mode 100644 index 00000000000..8ad184553f4 --- /dev/null +++ b/src/lib/account/aptos.ts @@ -0,0 +1,28 @@ +import { aptosChainId, toAccountId } from '@shapeshiftoss/caip' +import { supportsAptos } from '@shapeshiftoss/hdwallet-core/wallet' +import type { AccountMetadataById } from '@shapeshiftoss/types' + +import type { DeriveAccountIdsAndMetadata } from './account' + +import { assertGetAptosChainAdapter } from '@/lib/utils/aptos' + +export const deriveAptosAccountIdsAndMetadata: DeriveAccountIdsAndMetadata = async args => { + const { accountNumber, chainIds, wallet } = args + + if (!supportsAptos(wallet)) return {} + + const result: AccountMetadataById = {} + for (const chainId of chainIds) { + if (chainId !== aptosChainId) continue + + const adapter = assertGetAptosChainAdapter(chainId) + const bip44Params = adapter.getBip44Params({ accountNumber }) + + const address = await adapter.getAddress({ accountNumber, wallet }) + + const accountId = toAccountId({ chainId, account: address }) + result[accountId] = { bip44Params } + } + + return result +} diff --git a/src/lib/utils/aptos.ts b/src/lib/utils/aptos.ts new file mode 100644 index 00000000000..1bd797bf817 --- /dev/null +++ b/src/lib/utils/aptos.ts @@ -0,0 +1,45 @@ +import type { ChainId } from '@shapeshiftoss/caip' +import { aptosChainId } from '@shapeshiftoss/caip' +import type { aptos } from '@shapeshiftoss/chain-adapters' +import type { KnownChainIds } from '@shapeshiftoss/types' +import { TxStatus } from 'packages/unchained-client/src/types' + +import { getChainAdapterManager } from '@/context/PluginProvider/chainAdapterSingleton' + +export const isAptosChainAdapter = (chainAdapter: unknown): chainAdapter is aptos.ChainAdapter => { + if (!chainAdapter || typeof chainAdapter !== 'object') return false + return (chainAdapter as aptos.ChainAdapter).getChainId() === aptosChainId +} + +export const assertGetAptosChainAdapter = ( + chainId: ChainId | KnownChainIds, +): aptos.ChainAdapter => { + const chainAdapterManager = getChainAdapterManager() + const adapter = chainAdapterManager.get(chainId) + + if (!isAptosChainAdapter(adapter)) { + throw Error('invalid chain adapter') + } + + return adapter +} + +export const getAptosTransactionStatus = async (txHash: string): Promise => { + try { + const adapter = assertGetAptosChainAdapter(aptosChainId) + const rpcUrl = (adapter as unknown as { rpcUrl: string }).rpcUrl + + const response = await fetch(`${rpcUrl}/transactions/by_hash/${txHash}`) + if (!response.ok) return TxStatus.Unknown + + const tx = await response.json() + + if (tx.success === false) return TxStatus.Failed + if (tx.success === true) return TxStatus.Confirmed + + return TxStatus.Unknown + } catch (error) { + console.error('Error getting Aptos transaction status:', error) + return TxStatus.Unknown + } +} diff --git a/src/state/slices/portfolioSlice/utils/index.ts b/src/state/slices/portfolioSlice/utils/index.ts index 9e4d64108b1..5dc1dfcfd55 100644 --- a/src/state/slices/portfolioSlice/utils/index.ts +++ b/src/state/slices/portfolioSlice/utils/index.ts @@ -1,6 +1,7 @@ import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip' import { abstractChainId, + aptosChainId, arbitrumChainId, ASSET_NAMESPACE, avalancheChainId, @@ -64,6 +65,7 @@ import { isGridPlus, isPhantom, supportsAbstract, + supportsAptos, supportsArbitrum, supportsAvalanche, supportsBase, @@ -177,6 +179,7 @@ export const accountIdToLabel = (accountId: AccountId): string => { case suiChainId: case nearChainId: case tonChainId: + case aptosChainId: return middleEllipsis(pubkey) case btcChainId: // TODO(0xdef1cafe): translations @@ -441,6 +444,27 @@ export const accountToPortfolio: AccountToPortfolio = ({ assetIds, portfolioAcco break } + case CHAIN_NAMESPACE.Aptos: { + const aptosAccount = account as Account + const { chainId, assetId, pubkey } = account + const accountId = toAccountId({ chainId, account: pubkey }) + + portfolio.accounts.ids.push(accountId) + portfolio.accounts.byId[accountId] = { assetIds: [assetId], hasActivity } + portfolio.accountBalances.ids.push(accountId) + portfolio.accountBalances.byId[accountId] = { [assetId]: account.balance } + + aptosAccount.chainSpecific.tokens?.forEach(token => { + if (!assetIds.includes(token.assetId)) return + + if (bnOrZero(token.balance).gt(0)) portfolio.accounts.byId[accountId].hasActivity = true + + portfolio.accounts.byId[accountId].assetIds.push(token.assetId) + portfolio.accountBalances.byId[accountId][token.assetId] = token.balance + }) + + break + } default: assertUnreachable(chainNamespace) } @@ -520,6 +544,11 @@ export const checkAccountHasActivity = (account: Account) => { return hasActivity } + case CHAIN_NAMESPACE.Aptos: { + const hasActivity = bnOrZero(account.balance).gt(0) + + return hasActivity + } default: assertUnreachable(chainNamespace) } @@ -625,6 +654,8 @@ export const isAssetSupportedByWallet = (assetId: AssetId, wallet: HDWallet): bo return supportsNear(wallet) case tonChainId: return supportsTon(wallet) + case aptosChainId: + return supportsAptos(wallet) default: return false } @@ -868,4 +899,20 @@ export const makeAssets = async ({ { byId: {}, ids: [] }, ) } + + if (chainId === aptosChainId) { + const account = portfolioAccounts[pubkey] as Account + + return (account.chainSpecific.tokens ?? []).reduce( + (prev, token) => { + if (state.assets.byId[token.assetId]) return prev + + prev.byId[token.assetId] = makeAsset(state.assets.byId, { ...token }) + prev.ids.push(token.assetId) + + return prev + }, + { byId: {}, ids: [] }, + ) + } } From fcb9beded019eda9eb5048c11a4a7a910d15c715 Mon Sep 17 00:00:00 2001 From: Discostu Date: Thu, 14 May 2026 23:58:07 +0200 Subject: [PATCH 07/16] feat: add Aptos swapper types to Swapper/SwapperApi (web-ohh.10) --- packages/swapper/src/types.ts | 38 ++++++++++++++++++++++++++++++++--- packages/swapper/src/utils.ts | 10 ++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index 62a579860f0..756fd13f7c1 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -1,5 +1,6 @@ import type { AccountId, AssetId, ChainId, Nominal } from '@shapeshiftoss/caip' import type { + aptos, ChainAdapter, CosmosSdkChainAdapter, EvmChainAdapter, @@ -13,6 +14,7 @@ import type { UtxoChainAdapter, } from '@shapeshiftoss/chain-adapters' import type { + AptosSignTx, HDWallet, SolanaSignTx, StarknetSignTx, @@ -179,6 +181,11 @@ export type SuiFeeData = { gasPrice: string } +export type AptosFeeData = { + gasUnitPrice: string + maxGasAmount: string +} + export type AmountDisplayMeta = { amountCryptoBaseUnit: string asset: Partial & Pick @@ -189,7 +196,7 @@ export type ProtocolFee = { requiresBalance: boolean } & AmountDisplayMeta export type QuoteFeeData = { networkFeeCryptoBaseUnit: string | undefined // fee paid to the network from the fee asset (undefined if unknown) protocolFees: PartialRecord | undefined // fee(s) paid to the protocol(s) - chainSpecific?: UtxoFeeData | CosmosSdkFeeData | SolanaFeeData | SuiFeeData + chainSpecific?: UtxoFeeData | CosmosSdkFeeData | SolanaFeeData | SuiFeeData | AptosFeeData } export type BuyAssetBySellIdInput = { @@ -377,6 +384,10 @@ export type TonSwapperDeps = { assertGetTonChainAdapter: (chainId: ChainId) => ton.ChainAdapter } +export type AptosSwapperDeps = { + assertGetAptosChainAdapter: (chainId: ChainId) => aptos.ChainAdapter +} + export type SwapperDeps = { assetsById: AssetsByIdPartial config: SwapperConfig @@ -390,7 +401,8 @@ export type SwapperDeps = { SuiSwapperDeps & NearSwapperDeps & StarknetSwapperDeps & - TonSwapperDeps + TonSwapperDeps & + AptosSwapperDeps export type AffiliateFee = { assetId: AssetId @@ -734,6 +746,10 @@ export type TonTransactionExecutionProps = { signAndBroadcastTransaction: (txToSign: ton.TonSignTx) => Promise } +export type AptosTransactionExecutionProps = { + signAndBroadcastTransaction: (txToSign: AptosSignTx) => Promise +} + type EvmAccountMetadata = { from: string } type SolanaAccountMetadata = { from: string } type TronAccountMetadata = { from: string } @@ -784,6 +800,11 @@ export type GetUnsignedTonTransactionArgs = CommonGetUnsignedTransactionArgs & TonAccountMetadata & TonSwapperDeps +type AptosAccountMetadata = { from: string } +export type GetUnsignedAptosTransactionArgs = CommonGetUnsignedTransactionArgs & + AptosAccountMetadata & + AptosSwapperDeps + export type GetUnsignedEvmMessageArgs = CommonGetUnsignedTransactionArgs & EvmAccountMetadata & Omit @@ -823,7 +844,8 @@ export type CheckTradeStatusInput = { SuiSwapperDeps & NearSwapperDeps & StarknetSwapperDeps & - TonSwapperDeps + TonSwapperDeps & + AptosSwapperDeps export type TradeStatus = { status: TxStatus @@ -892,6 +914,10 @@ export type Swapper = { txToSign: ton.TonSignTx, callbacks: TonTransactionExecutionProps, ) => Promise + executeAptosTransaction?: ( + txToSign: AptosSignTx, + callbacks: AptosTransactionExecutionProps, + ) => Promise } export type SwapperApi = { @@ -918,6 +944,7 @@ export type SwapperApi = { input: GetUnsignedStarknetTransactionArgs, ) => Promise getUnsignedTonTransaction?: (input: GetUnsignedTonTransactionArgs) => Promise + getUnsignedAptosTransaction?: (input: GetUnsignedAptosTransactionArgs) => Promise getEvmTransactionFees?: (input: GetUnsignedEvmTransactionArgs) => Promise getSolanaTransactionFees?: (input: GetUnsignedSolanaTransactionArgs) => Promise @@ -928,6 +955,7 @@ export type SwapperApi = { getNearTransactionFees?: (input: GetUnsignedNearTransactionArgs) => Promise getStarknetTransactionFees?: (input: GetUnsignedStarknetTransactionArgs) => Promise getTonTransactionFees?: (input: GetUnsignedTonTransactionArgs) => Promise + getAptosTransactionFees?: (input: GetUnsignedAptosTransactionArgs) => Promise } export type QuoteResult = Result & { @@ -989,6 +1017,10 @@ export type TonTransactionExecutionInput = CommonTradeExecutionInput & TonTransactionExecutionProps & TonAccountMetadata +export type AptosTransactionExecutionInput = CommonTradeExecutionInput & + AptosTransactionExecutionProps & + AptosAccountMetadata + export enum TradeExecutionEvent { SellTxHash = 'sellTxHash', RelayerTxHash = 'relayerTxHash', diff --git a/packages/swapper/src/utils.ts b/packages/swapper/src/utils.ts index 66ae91f4572..a3fbc967194 100644 --- a/packages/swapper/src/utils.ts +++ b/packages/swapper/src/utils.ts @@ -11,7 +11,7 @@ import type { } from '@shapeshiftoss/chain-adapters' import { isSecondClassEvmAdapter } from '@shapeshiftoss/chain-adapters' import type { TronSignTx } from '@shapeshiftoss/chain-adapters/src/tron/types' -import type { SolanaSignTx, StarknetSignTx, SuiSignTx } from '@shapeshiftoss/hdwallet-core' +import type { AptosSignTx, SolanaSignTx, StarknetSignTx, SuiSignTx } from '@shapeshiftoss/hdwallet-core' import type { Asset, EvmChainId } from '@shapeshiftoss/types' import { evm, TxStatus } from '@shapeshiftoss/unchained-client' import { BigAmount, bn } from '@shapeshiftoss/utils' @@ -23,6 +23,7 @@ import { setupCache } from 'axios-cache-interceptor' import { fetchSafeTransactionInfo } from './safe-utils' import type { + AptosTransactionExecutionProps, EvmTransactionExecutionProps, ExecutableTradeStep, NearTransactionExecutionProps, @@ -227,6 +228,13 @@ export const executeTonTransaction = ( return callbacks.signAndBroadcastTransaction(txToSign) } +export const executeAptosTransaction = ( + txToSign: AptosSignTx, + callbacks: AptosTransactionExecutionProps, +) => { + return callbacks.signAndBroadcastTransaction(txToSign) +} + export const createDefaultStatusResponse = (buyTxHash?: string) => ({ status: TxStatus.Unknown, buyTxHash, From dacf127a6a4736cf02d58afc2f69bf4b6ad23d40 Mon Sep 17 00:00:00 2001 From: Discostu Date: Sat, 16 May 2026 22:06:50 +0200 Subject: [PATCH 08/16] feat(aptos): complete Aptos integration wiring Wires the Aptos chain adapter, HDWallet, and types (added in prior commits) into the full app: asset generation via Panora token list, CoinGecko adapter, plugin indexer URL, NEAR Intents Aptos support (web-xeq.3), RFOX bridge, send modal, trade execution dispatch, state migrations bump (v334), e2e fixtures, and ButterSwap test mock updates for the AptosSwapperDeps ripple. Also fixes audit findings: - getRpcUrl() on AptosChainAdapter to replace unsafe protected cast - VITE_APTOS_INDEXER_URL validation + type declaration - Record typing on test fixture marketDataByAssetIdUsd Refs: web-xeq.1 (chain adapter), web-xeq.3 (NEAR Intents Aptos), web-xeq.4 (qabot fixtures). PanoraSwapper (web-xeq.2) intentionally deferred to a separate PR. Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.production | 1 + e2e/fixtures/aptos-chain-integration.yaml | 66 +++ headers/csps/chains/aptos.ts | 2 + .../generated/aptos_861fb8e6/adapter.json | 1 + .../src/adapters/coingecko/generated/index.ts | 2 + packages/caip/src/adapters/coingecko/index.ts | 2 +- packages/caip/src/adapters/coingecko/utils.ts | 4 +- packages/chain-adapters/package.json | 1 + .../src/aptos/AptosChainAdapter.test.ts | 300 +++++++++++++ .../src/aptos/AptosChainAdapter.ts | 395 ++++++++++-------- packages/chain-adapters/src/types.ts | 2 +- packages/hdwallet-core/src/aptos.ts | 16 +- packages/hdwallet-core/src/wallet.ts | 6 +- packages/hdwallet-native/src/aptos.ts | 4 +- .../src/crypto/isolation/adapters/aptos.ts | 17 +- packages/hdwallet-native/src/native.ts | 50 ++- packages/public-api/src/swapperDeps.ts | 3 + packages/swapper/package.json | 1 + .../swapperApi/getTradeQuote.test.ts | 1 + .../swapperApi/getTradeRate.test.ts | 2 + .../NearIntentsSwapper/NearIntentsSwapper.ts | 2 + .../swappers/NearIntentsSwapper/endpoints.ts | 38 ++ .../swapperApi/getTradeQuote.ts | 11 + .../swapperApi/getTradeRate.ts | 20 + .../src/swappers/NearIntentsSwapper/types.ts | 2 + .../src/swappers/utils/helpers/helpers.ts | 2 + .../utils/test-data/cryptoMarketDataById.ts | 4 +- .../src/thorchain-utils/getL1RateOrQuote.ts | 8 + packages/swapper/src/utils.ts | 44 +- packages/utils/src/assetData/baseAssets.ts | 2 +- packages/utils/src/assetData/getBaseAsset.ts | 2 +- packages/utils/src/chainIdToFeeAssetId.ts | 2 +- .../utils/src/getNativeFeeAssetReference.ts | 7 + packages/utils/src/treasury.ts | 5 + scripts/generateAssetData/aptos/index.ts | 81 ++++ scripts/generateAssetData/coingecko.ts | 10 + .../generateAssetData/generateAssetData.ts | 3 + .../generateChainRelatedAssetIndex.ts | 2 + .../generateRelatedAssetIndex.ts | 2 + .../generateTrustWalletUrl.ts | 1 + src/components/Modals/Send/utils.ts | 27 ++ .../TradeConfirm/hooks/useTradeExecution.tsx | 35 ++ .../useTradeNetworkFeeCryptoBaseUnit.tsx | 22 + .../getTradeQuoteOrRateInput.ts | 19 + .../hooks/useGetPopularAssetsQuery.tsx | 2 + src/config.ts | 1 + src/constants/chains.ts | 2 + src/context/PluginProvider/PluginProvider.tsx | 1 + .../WalletProvider/Ledger/constants.ts | 2 + .../useWalletSupportsChain.ts | 7 +- src/lib/account/account.ts | 1 - src/lib/asset-service/service/AssetService.ts | 2 + src/lib/coingecko/constants.ts | 2 + src/lib/coingecko/utils.ts | 2 + src/lib/tradeExecution.ts | 54 +++ src/lib/utils/aptos.ts | 2 +- src/pages/Markets/components/MarketsRow.tsx | 3 + .../Stake/Bridge/hooks/useRfoxBridge.ts | 2 + src/plugins/activePlugins.ts | 2 + src/plugins/aptos/index.tsx | 1 + .../apis/swapper/helpers/swapperApiHelpers.ts | 2 + .../slices/opportunitiesSlice/mappings.ts | 1 + src/vite-env.d.ts | 1 + 63 files changed, 1095 insertions(+), 222 deletions(-) create mode 100644 e2e/fixtures/aptos-chain-integration.yaml create mode 100644 packages/caip/src/adapters/coingecko/generated/aptos_861fb8e6/adapter.json create mode 100644 packages/chain-adapters/src/aptos/AptosChainAdapter.test.ts create mode 100644 scripts/generateAssetData/aptos/index.ts diff --git a/.env.production b/.env.production index a8162c25ac2..eb2b80ae986 100644 --- a/.env.production +++ b/.env.production @@ -6,6 +6,7 @@ VITE_FEATURE_THORCHAIN_TCY_ACTIVITY=false VITE_FEATURE_AGENTIC_CHAT=false VITE_FEATURE_FLOWEVM=false VITE_FEATURE_CELO=false +VITE_FEATURE_APTOS=false # mixpanel VITE_MIXPANEL_TOKEN=9d304465fc72224aead9e027e7c24356 diff --git a/e2e/fixtures/aptos-chain-integration.yaml b/e2e/fixtures/aptos-chain-integration.yaml new file mode 100644 index 00000000000..33016f0b9af --- /dev/null +++ b/e2e/fixtures/aptos-chain-integration.yaml @@ -0,0 +1,66 @@ +name: Aptos Chain Integration +description: Validate Aptos chain integration - account discovery, balance display, native APT visibility, and asset picker integration. Mirrors the chain-integration-template.md test scenario used for Solana. +route: /trade +depends_on: + - wallet-health.yaml +steps: + - name: Open Manage Accounts modal + instruction: > + After wallet is unlocked and trade page is loaded, click the wallet button in the + top right (shows wallet name e.g. "teest"). Then click the three-dot menu icon + and select "Manage Accounts". + expected: Manage Accounts modal opened, list of supported chains displayed + screenshot: true + + - name: Verify Aptos appears in chain list + instruction: > + In the Manage Accounts modal, scroll through the supported chains list and + verify "Aptos" appears as one of the available chains. + expected: Aptos is visible in the supported chains list + screenshot: true + + - name: Discover Aptos account + instruction: > + Click on the Aptos chain row to expand it. If no account is shown, click + "Add Account" or the "+" button to derive the first Aptos account + (BIP44 path m/44'/637'/0'/0'/0'). Wait for the address to appear. + expected: An Aptos account address (0x followed by 64 hex chars) is shown + screenshot: true + + - name: Close modal and verify Aptos in portfolio + instruction: > + Close the Manage Accounts modal. Navigate to the Dashboard (or Portfolio) page. + Look for Aptos chain in the asset list, or filter by chain to confirm Aptos + assets appear. + expected: Aptos chain and its native asset APT are visible in the portfolio + screenshot: true + + - name: Open asset picker and search for APT + instruction: > + Navigate to /trade. Click the sell asset selector button. In the asset picker + dialog, type "APT" in the search box. Wait for the results to filter. + expected: APT (Aptos Coin) appears in the search results + screenshot: true + + - name: Select APT as sell asset + instruction: > + Click "APT" (Aptos Coin, Aptos chain) from the search results. Wait for the + dialog to close and APT to appear as the sell asset. + expected: APT is selected as the sell asset, the chain filter shows Aptos + screenshot: true + + - name: Verify Aptos chain filter in buy asset picker + instruction: > + Click the buy/receive asset selector. In the asset picker dialog, find and click + the Aptos chain filter chip (if available). Verify that filtering shows multiple + Aptos assets (e.g. APT, USDC, USDT, MOD, thAPT). + expected: Multiple Aptos-chain assets are listed (>= 5 tokens) + screenshot: true + + - name: Verify APT native balance reads correctly + instruction: > + Close the asset picker. Look at the APT balance shown next to the sell input + ("Balance: X APT" or similar). Verify the balance is a numeric value (could be + 0 if test wallet has no APT, but should not be "N/A" or an error). + expected: APT balance is displayed as a numeric value (including 0) + screenshot: true diff --git a/headers/csps/chains/aptos.ts b/headers/csps/chains/aptos.ts index d3879543557..2a630a344aa 100644 --- a/headers/csps/chains/aptos.ts +++ b/headers/csps/chains/aptos.ts @@ -8,6 +8,8 @@ const env = loadEnv(mode, process.cwd(), '') export const csp: Csp = { 'connect-src': [ env.VITE_APTOS_NODE_URL, + env.VITE_APTOS_INDEXER_URL, 'https://fullnode.mainnet.aptoslabs.com/', + 'https://api.mainnet.aptoslabs.com/', ], } diff --git a/packages/caip/src/adapters/coingecko/generated/aptos_861fb8e6/adapter.json b/packages/caip/src/adapters/coingecko/generated/aptos_861fb8e6/adapter.json new file mode 100644 index 00000000000..516ba15f950 --- /dev/null +++ b/packages/caip/src/adapters/coingecko/generated/aptos_861fb8e6/adapter.json @@ -0,0 +1 @@ +{"aptos:861fb8e6/slip44:637": "aptos", "aptos:861fb8e6/coin:0x357b0b74bc833e95a115ad22604854d6b0fca151cecd94111770e5d6ffc9dc2b": "tether", "aptos:861fb8e6/coin:0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b": "usd-coin", "aptos:861fb8e6/coin:0x05fabd1b12e39967a3c24e91b7b8f67719a6dacee74f3c8b9fb7d93e855437d2": "usd1-wlfi", "aptos:861fb8e6/coin:0x68844a0d7f2587e726ad0579f3d640865bb4162c08a4589eeda3f9689ec52a3d": "wrapped-bitcoin", "aptos:861fb8e6/coin:0x435ad41e7b383cef98899c4e5a22c8dc88ab67b22f95e5663d6c6649298c3a9d": "hyperion", "aptos:861fb8e6/coin:0x878370592f9129e14b76558689a4b570ad22678111df775befbfcbc9fb3d90ab": "merkle-trade", "aptos:861fb8e6/coin:0xb36527754eb54d7ff55daf13bcb54b42b88ec484bd6f0e3b2e0d1db169de6451": "ami", "aptos:861fb8e6/coin:0x02370cc1d995f3aadd337c1c6c63834ad8d2bd0cdc70bc8dff81de463e18b159": "pontem-liquidswap", "aptos:861fb8e6/coin:0x377adc4848552eb2ea17259be928001923efe12271fef1667e2b784f04a7cf3a": "thala", "aptos:861fb8e6/coin:0x2ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12": "cellena-finance", "aptos:861fb8e6/coin:0x0009da434d9b873b5159e8eeed70202ad22dc075867a7793234fbc981b63e119": "gui-inu", "aptos:861fb8e6/coin:0x378d5ba871c3d1bdf477a617f997f23d9e0702de97a02f42925b44fa3abc9866": "meso-finance", "aptos:861fb8e6/coin:0x79e8a5ddb82aa53854c1348c2865fd00732a41937dc1c160a4a50205537bd740": "tomarket", "aptos:861fb8e6/coin:0xb27b0c6b60772f0fc804ec1cd3339f552badf9bd1e125a7dd700d8eb11248ef1": "doodoo", "aptos:861fb8e6/coin:0xf37a8864fe737eb8ec2c2931047047cbaed1beed3fb0e5b7c5526dafd3b9c2e9": "ethena-usde", "aptos:861fb8e6/coin:0xa259be733b6a759909f92815927fa213904df6540519568692caf0b068fe8e62": "amnis-aptos", "aptos:861fb8e6/coin:0xb614bfdf9edc39b330bbf9c3c5bcd0473eee2f6d4e21748629cc367869ece627": "amnis-staked-aptos-coin", "aptos:861fb8e6/coin:0xa0d9d647c5737a5aed08d2cfeb39c31cf901d44bc4aa024eaa7e5e68b804e011": "thala-apt", "aptos:861fb8e6/coin:0x0a9ce1bddf93b074697ec5e483bc5050bc64cff2acd31e1ccfd8ac8cae5e4abe": "staked-thala-apt", "aptos:861fb8e6/coin:0x821c94e69bc7ca058c913b7b5e6b0a5c9fd1523d58723a966fb8c1f5ea888105": "kofi-aptos", "aptos:861fb8e6/coin:0x42556039b88593e768c97ab1a3ab0c6a17230825769304482dff8fdebe4c002b": "staked-kofi-aptos", "aptos:861fb8e6/coin:0x94ed76d3d66cb0b6e7a3ab81acf830e3a50b8ae3cfb9edc0abea635a11185ff4": "move-dollar", "aptos:861fb8e6/coin:0x2b3be0a97a73c87ff62cbdd36837a9fb5bbd1d7f06a73b7ed62ec15c5326c1b8": "layerzero-bridged-usdc-aptos", "aptos:861fb8e6/coin:0xe568e9322107a5c9ba4cbd05a630a5586aa73e744ada246c3efb0f4ce3e295f3": "layerzero-bridged-usdt-aptos", "aptos:861fb8e6/coin:0xae02f68520afd221a5cd6fda6f5500afedab8d0a2e19a916d6d8bc2b36e758db": "layerzero-bridged-weth-aptos", "aptos:861fb8e6/coin:0x54fc0d5fa5ad975ede1bf8b1c892ae018745a1afd4a4da9b70bb6e5448509fc0": "bridged-usd-coin-wormhole-ethereum", "aptos:861fb8e6/coin:0xf599112bc3a5b6092469890d6a2f353f485a6193c9d36626b480704467d3f4c8": "abtc", "aptos:861fb8e6/coin:0x8e51106b139001f1f25a320066621a2e0d140724ee9be1d49aaf9e76ceb24d75": "bedrock-btc", "aptos:861fb8e6/coin:0xf764dbfd6999067ac052a8e722ae359bec389bd7dba19ead586801b99b81b075": "universal-btc", "aptos:861fb8e6/coin:0x5915ae0eae3701833fa02e28bf530bc01ca96a5f010ac8deecb14c7a92661368": "uptos", "aptos:861fb8e6/coin:0x1fe81b3886ff129d42064f7ee934013de7ef968cb8f47adb5f7210192bcd4a23": "chewy-token", "aptos:861fb8e6/coin:0xa0fa5918da73235921c6120597db820df0be391d0056dc0a7ee7a80b83f29d64": "moo-moo", "aptos:861fb8e6/coin:0x4c3efb98d8d3662352f331b3465c6df263d1a7e84f002844348519614a5fea30": "movegpt", "aptos:861fb8e6/coin:0xd08a1ab00895c35dd19b356f5747355ebcd58cf5e684c15e6808d760ffd6beff": "hair", "aptos:861fb8e6/coin:0xad18575b0e51dd056e1e082223c0e014cbfe4b13bc55e92f450585884f4cf951": "pancakeswap-token", "aptos:861fb8e6/coin:0xaef6a8c3182e076db72d64324617114cacf9a52f28325edc10b483f7f05da0e7": "trufin-staked-apt", "aptos:861fb8e6/coin:0xa64d2d6f5e26daf6a3552f51d4110343b1a8c8046d0a9e72fa4086a337f3236c": "layerzero-bridged-wbtc-aptos", "aptos:861fb8e6/coin:0xc692943f7b340f02191c5de8dac2f827e0b66b3ed2206206a3526bcb0cae6e40": "cash-2", "aptos:861fb8e6/coin:0xb30a694a344edee467d9f82330bbe7c3b89f440a1ecd2da1f3bca266560fce69": "ethena-staked-usde", "aptos:861fb8e6/coin:0x92410a41654236295001f06375afbb1786dbd14bc1c42a33bfcf50204c248bb7": "ethereum-wormhole", "aptos:861fb8e6/coin:0x700e285ee9f4fc9b0e42a6217e329899e1353476bc532a484048008c8bc8e400": "stakestone-ether", "aptos:861fb8e6/coin:0xfbd6406c12cab2aef728c917a365cdb73883213f74af5e8a46c8fbd77b623ee7": "wrapped-ether-celer", "aptos:861fb8e6/coin:0x6dba1728c73363be1bdd4d504844c40fbb893e368ccbeff1d1bd83497dbc756d": "propbase", "aptos:861fb8e6/coin:0xe528f4df568eb9fff6398adc514bc9585fab397f478972bcbebf1e75dee40a88": "apollo-diversified-credit-securitize-fund", "aptos:861fb8e6/coin:0x50038be55be5b964cfa32cf128b5cf05f123959f286b4cc02b86cafd48945f89": "blackrock-usd-institutional-digital-liquidity-fund"} \ No newline at end of file diff --git a/packages/caip/src/adapters/coingecko/generated/index.ts b/packages/caip/src/adapters/coingecko/generated/index.ts index e8e5bb68b88..330503cd79c 100644 --- a/packages/caip/src/adapters/coingecko/generated/index.ts +++ b/packages/caip/src/adapters/coingecko/generated/index.ts @@ -46,6 +46,7 @@ import tron from "./tron_0x2b6653dc/adapter.json"; import zcash from "./bip122_00040fe8ec8471911baa1db1266ea15d/adapter.json"; import near from "./near_mainnet/adapter.json"; import ton from "./ton_mainnet/adapter.json"; +import aptos from "./aptos_861fb8e6/adapter.json"; export { bitcoin, @@ -96,4 +97,5 @@ export { zcash, near, ton, + aptos, }; diff --git a/packages/caip/src/adapters/coingecko/index.ts b/packages/caip/src/adapters/coingecko/index.ts index 369df00d525..3300f96338d 100644 --- a/packages/caip/src/adapters/coingecko/index.ts +++ b/packages/caip/src/adapters/coingecko/index.ts @@ -6,6 +6,7 @@ import type { ChainId } from '../../chainId/chainId' import { fromChainId, toChainId } from '../../chainId/chainId' import { abstractChainId, + aptosChainId, arbitrumChainId, avalancheChainId, baseChainId, @@ -44,7 +45,6 @@ import { starknetChainId, storyChainId, suiChainId, - aptosChainId, thorchainChainId, tonChainId, tronChainId, diff --git a/packages/caip/src/adapters/coingecko/utils.ts b/packages/caip/src/adapters/coingecko/utils.ts index b00b3548ff8..2c632fb88e1 100644 --- a/packages/caip/src/adapters/coingecko/utils.ts +++ b/packages/caip/src/adapters/coingecko/utils.ts @@ -7,6 +7,8 @@ import type { ChainId } from '../../chainId/chainId' import { abstractAssetId, abstractChainId, + aptosAssetId, + aptosChainId, arbitrumAssetId, arbitrumChainId, ASSET_NAMESPACE, @@ -81,8 +83,6 @@ import { storyAssetId, storyChainId, suiAssetId, - aptosAssetId, - aptosChainId, suiChainId, thorchainChainId, tonAssetId, diff --git a/packages/chain-adapters/package.json b/packages/chain-adapters/package.json index f8af39f8374..17a5487b0aa 100644 --- a/packages/chain-adapters/package.json +++ b/packages/chain-adapters/package.json @@ -29,6 +29,7 @@ "postbuild:cjs": "echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json" }, "dependencies": { + "@aptos-labs/ts-sdk": "6.3.1", "@mysten/sui": "1.45.0", "@near-js/crypto": "^2.5.1", "@near-js/providers": "^2.5.1", diff --git a/packages/chain-adapters/src/aptos/AptosChainAdapter.test.ts b/packages/chain-adapters/src/aptos/AptosChainAdapter.test.ts new file mode 100644 index 00000000000..c8694bc2681 --- /dev/null +++ b/packages/chain-adapters/src/aptos/AptosChainAdapter.test.ts @@ -0,0 +1,300 @@ +import { aptosAssetId, aptosChainId } from '@shapeshiftoss/caip' +import { KnownChainIds } from '@shapeshiftoss/types' +import { TransferType, TxStatus } from '@shapeshiftoss/unchained-client' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { ValidAddressResultType } from '../types' +import { ChainAdapter } from './AptosChainAdapter' + +vi.mock('@aptos-labs/ts-sdk', () => { + return { + Aptos: vi.fn().mockImplementation(() => ({ + getAccountCoinsData: vi.fn(), + getGasPriceEstimation: vi.fn(), + account: { getAccountInfo: vi.fn() }, + transaction: { getTransactionByHash: vi.fn() }, + })), + AptosConfig: vi.fn(), + Network: { MAINNET: 'mainnet' }, + } +}) + +const ADDR = '0x304ba231cacfd0b8ee2b3b3b0aa8ef3648f4efffa7080be996c57c107750eb22' +const SENDER = '0xd1a1c1804e91ba85a569c7f018bb7502d2f13d4742d2611953c9c14681af6446' +const APT_COIN_TYPE = '0x1::aptos_coin::AptosCoin' +const USDC_FA = '0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b' + +const newAdapter = () => + new ChainAdapter({ + rpcUrl: 'https://fullnode.mainnet.aptoslabs.com/v1', + indexerUrl: 'https://api.mainnet.aptoslabs.com/v1/graphql', + }) + +const getMockedClient = (adapter: ChainAdapter) => + (adapter as unknown as { client: Record }).client + +describe('AptosChainAdapter', () => { + let adapter: ChainAdapter + let client: Record + + beforeEach(() => { + adapter = newAdapter() + client = getMockedClient(adapter) + }) + + describe('basic getters', () => { + it('exposes the canonical chainId and assetId', () => { + expect(adapter.getChainId()).toBe(aptosChainId) + expect(adapter.getFeeAssetId()).toBe(aptosAssetId) + expect(adapter.getType()).toBe(KnownChainIds.AptosMainnet) + }) + + it('builds hardened BIP44 params on coin type 637', () => { + const params = adapter.getBip44Params({ accountNumber: 0 }) + expect(params).toMatchObject({ + purpose: 44, + coinType: 637, + accountNumber: 0, + addressIndex: 0, + isChange: false, + }) + }) + + it('rejects negative account numbers', () => { + expect(() => adapter.getBip44Params({ accountNumber: -1 })).toThrow() + }) + }) + + describe('validateAddress', () => { + it('accepts 64-hex address with 0x prefix', async () => { + const r = await adapter.validateAddress(ADDR) + expect(r).toEqual({ valid: true, result: ValidAddressResultType.Valid }) + }) + + it('rejects addresses without 0x prefix', async () => { + const r = await adapter.validateAddress(ADDR.slice(2)) + expect(r.valid).toBe(false) + }) + + it('rejects addresses of wrong length', async () => { + const r = await adapter.validateAddress('0xabcd') + expect(r.valid).toBe(false) + }) + + it('rejects non-hex content', async () => { + const r = await adapter.validateAddress('0x' + 'z'.repeat(64)) + expect(r.valid).toBe(false) + }) + }) + + describe('getAccount', () => { + it('returns native APT balance when only APT is held', async () => { + client.getAccountCoinsData.mockResolvedValueOnce([ + { + asset_type: APT_COIN_TYPE, + amount: '103970825', + metadata: { symbol: 'APT', name: 'Aptos Coin', decimals: 8 }, + }, + ]) + + const acct = await adapter.getAccount(ADDR) + expect(acct.balance).toBe('103970825') + expect(acct.assetId).toBe(aptosAssetId) + expect(acct.chainSpecific.tokens).toEqual([]) + }) + + it('populates chainSpecific.tokens with FA tokens, keeping APT separate', async () => { + client.getAccountCoinsData.mockResolvedValueOnce([ + { + asset_type: APT_COIN_TYPE, + amount: '103970825', + metadata: { symbol: 'APT', name: 'Aptos Coin', decimals: 8 }, + }, + { + asset_type: USDC_FA, + amount: '5000000', + metadata: { symbol: 'USDC', name: 'USD Coin', decimals: 6 }, + }, + ]) + + const acct = await adapter.getAccount(ADDR) + expect(acct.balance).toBe('103970825') + expect(acct.chainSpecific.tokens).toEqual([ + { + assetId: `aptos:861fb8e6/coin:${USDC_FA}`, + balance: '5000000', + symbol: 'USDC', + name: 'USD Coin', + precision: 6, + }, + ]) + }) + + it('filters out zero-amount entries', async () => { + client.getAccountCoinsData.mockResolvedValueOnce([ + { + asset_type: APT_COIN_TYPE, + amount: '0', + metadata: { symbol: 'APT', name: 'Aptos Coin', decimals: 8 }, + }, + { + asset_type: USDC_FA, + amount: '0', + metadata: { symbol: 'USDC', name: 'USD Coin', decimals: 6 }, + }, + ]) + + const acct = await adapter.getAccount(ADDR) + expect(acct.balance).toBe('0') + expect(acct.chainSpecific.tokens).toEqual([]) + }) + + it('falls back to UNKNOWN symbol when metadata is missing', async () => { + client.getAccountCoinsData.mockResolvedValueOnce([ + { asset_type: USDC_FA, amount: '42', metadata: null }, + ]) + + const acct = await adapter.getAccount(ADDR) + expect(acct.chainSpecific.tokens?.[0]).toMatchObject({ + symbol: 'UNKNOWN', + name: USDC_FA, + precision: 0, + }) + }) + }) + + describe('parseTx', () => { + const makeTx = (overrides: Record = {}) => ({ + hash: '0xtxhash', + version: '5276796244', + timestamp: '1747000000000000', // microseconds + success: true, + gas_used: '151', + gas_unit_price: '100', + sender: SENDER, + payload: { + function: '0x1::aptos_account::transfer_coins', + type_arguments: [APT_COIN_TYPE], + arguments: [ADDR, '9423057'], + }, + ...overrides, + }) + + it('attributes a Receive transfer for inbound APT', async () => { + client.transaction.getTransactionByHash.mockResolvedValueOnce(makeTx()) + const tx = await adapter.parseTx('0xtxhash', ADDR) + + expect(tx.status).toBe(TxStatus.Confirmed) + expect(tx.fee).toEqual({ assetId: aptosAssetId, value: '15100' }) + expect(tx.transfers).toEqual([ + { + assetId: aptosAssetId, + from: [SENDER], + to: [ADDR], + type: TransferType.Receive, + value: '9423057', + }, + ]) + }) + + it('attributes a Send transfer for outbound APT', async () => { + client.transaction.getTransactionByHash.mockResolvedValueOnce(makeTx()) + const tx = await adapter.parseTx('0xtxhash', SENDER) + + expect(tx.transfers).toEqual([ + { + assetId: aptosAssetId, + from: [SENDER], + to: [ADDR], + type: TransferType.Send, + value: '9423057', + }, + ]) + }) + + it('maps a FA primary_fungible_store::transfer to the right assetId and arg offsets', async () => { + client.transaction.getTransactionByHash.mockResolvedValueOnce( + makeTx({ + payload: { + function: '0x1::primary_fungible_store::transfer', + type_arguments: [], + arguments: [{ inner: USDC_FA }, ADDR, '5000000'], + }, + }), + ) + + const tx = await adapter.parseTx('0xtxhash', ADDR) + expect(tx.transfers).toEqual([ + { + assetId: `aptos:861fb8e6/coin:${USDC_FA}`, + from: [SENDER], + to: [ADDR], + type: TransferType.Receive, + value: '5000000', + }, + ]) + }) + + it('maps a legacy 0x1::aptos_account::transfer to APT', async () => { + client.transaction.getTransactionByHash.mockResolvedValueOnce( + makeTx({ + payload: { + function: '0x1::aptos_account::transfer', + type_arguments: [], + arguments: [ADDR, '1000'], + }, + }), + ) + + const tx = await adapter.parseTx('0xtxhash', ADDR) + expect(tx.transfers).toEqual([ + { + assetId: aptosAssetId, + from: [SENDER], + to: [ADDR], + type: TransferType.Receive, + value: '1000', + }, + ]) + }) + + it('returns Send+Receive when the user is both sender and recipient', async () => { + client.transaction.getTransactionByHash.mockResolvedValueOnce( + makeTx({ sender: ADDR, payload: { ...makeTx().payload, arguments: [ADDR, '100'] } }), + ) + + const tx = await adapter.parseTx('0xtxhash', ADDR) + expect(tx.transfers.map(t => t.type)).toEqual([TransferType.Send, TransferType.Receive]) + }) + + it('returns an empty transfer list for unrelated payloads', async () => { + client.transaction.getTransactionByHash.mockResolvedValueOnce( + makeTx({ + payload: { + function: '0x123::some_dapp::do_thing', + type_arguments: [], + arguments: [], + }, + }), + ) + + const tx = await adapter.parseTx('0xtxhash', ADDR) + expect(tx.transfers).toEqual([]) + }) + + it('marks failed transactions as Failed with zero confirmations', async () => { + client.transaction.getTransactionByHash.mockResolvedValueOnce(makeTx({ success: false })) + const tx = await adapter.parseTx('0xtxhash', ADDR) + expect(tx.status).toBe(TxStatus.Failed) + expect(tx.confirmations).toBe(0) + }) + + it('converts microsecond timestamps to seconds', async () => { + client.transaction.getTransactionByHash.mockResolvedValueOnce( + makeTx({ timestamp: '1747000000000000' }), + ) + const tx = await adapter.parseTx('0xtxhash', ADDR) + expect(tx.blockTime).toBe(1747000000) + }) + }) +}) diff --git a/packages/chain-adapters/src/aptos/AptosChainAdapter.ts b/packages/chain-adapters/src/aptos/AptosChainAdapter.ts index d1dfec4f21d..9b91f0a6ec6 100644 --- a/packages/chain-adapters/src/aptos/AptosChainAdapter.ts +++ b/packages/chain-adapters/src/aptos/AptosChainAdapter.ts @@ -1,10 +1,23 @@ +import type { InputEntryFunctionData } from '@aptos-labs/ts-sdk' +import { + AccountAuthenticatorEd25519, + Aptos, + AptosConfig, + Deserializer, + Ed25519PublicKey, + Ed25519Signature, + Network, + SimpleTransaction, +} from '@aptos-labs/ts-sdk' import type { AssetId, ChainId } from '@shapeshiftoss/caip' import { - ASSET_REFERENCE, aptosAssetId, aptosChainId, + ASSET_NAMESPACE, + ASSET_REFERENCE, + toAssetId, } from '@shapeshiftoss/caip' -import type { AptosSignTx, AptosWallet, HDWallet } from '@shapeshiftoss/hdwallet-core' +import type { AptosWallet, HDWallet } from '@shapeshiftoss/hdwallet-core' import { supportsAptos } from '@shapeshiftoss/hdwallet-core' import type { Bip44Params, RootBip44Params } from '@shapeshiftoss/types' import { KnownChainIds } from '@shapeshiftoss/types' @@ -29,11 +42,16 @@ import type { } from '../types' import { ChainAdapterDisplayName, ValidAddressResultType } from '../types' import { toAddressNList, verifyLedgerAppOpen } from '../utils' +import type { AptosToken } from './types' export interface ChainAdapterArgs { rpcUrl: string + indexerUrl: string } +const APT_COIN_TYPE = '0x1::aptos_coin::AptosCoin' +const MIN_MAX_GAS_AMOUNT = 12_000n + export class ChainAdapter implements IChainAdapter { static readonly rootBip44Params: RootBip44Params = { purpose: 44, @@ -44,9 +62,19 @@ export class ChainAdapter implements IChainAdapter { protected readonly chainId = aptosChainId protected readonly assetId = aptosAssetId protected readonly rpcUrl: string + protected readonly indexerUrl: string + protected readonly client: Aptos constructor(args: ChainAdapterArgs) { this.rpcUrl = args.rpcUrl + this.indexerUrl = args.indexerUrl + this.client = new Aptos( + new AptosConfig({ + network: Network.MAINNET, + fullnode: args.rpcUrl, + indexer: args.indexerUrl, + }), + ) } private assertSupportsChain(wallet: HDWallet): asserts wallet is AptosWallet { @@ -66,6 +94,10 @@ export class ChainAdapter implements IChainAdapter { return ChainAdapterDisplayName.Aptos } + getRpcUrl() { + return this.rpcUrl + } + getType(): KnownChainIds.AptosMainnet { return KnownChainIds.AptosMainnet } @@ -116,34 +148,42 @@ export class ChainAdapter implements IChainAdapter { async getAccount(pubkey: string): Promise> { try { - const response = await fetch(`${this.rpcUrl}/accounts/${pubkey}`) - - if (!response.ok) { - throw new Error(`Aptos account request failed: ${response.status}`) - } + const balances = await this.client.getAccountCoinsData({ + accountAddress: pubkey, + }) - const accountData = await response.json() + let nativeBalance = '0' + const tokens: AptosToken[] = [] - // Aptos returns coin balances as an array of { coin: { type: string }, coin?: { value: string } } - // The native APT balance is under 0x1::aptos_coin::AptosCoin - let balance = '0' + for (const entry of balances) { + if (!entry.asset_type || BigInt(entry.amount ?? 0) === 0n) continue - if (Array.isArray(accountData)) { - // Some endpoints return array of coin resources - for (const resource of accountData) { - if (resource.type === '0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>') { - balance = resource.data?.coin?.value ?? '0' - break - } + if (entry.asset_type === APT_COIN_TYPE) { + nativeBalance = String(entry.amount) + continue } + + const assetId = toAssetId({ + chainId: this.chainId, + assetNamespace: ASSET_NAMESPACE.aptosCoin, + assetReference: entry.asset_type, + }) + + tokens.push({ + assetId, + balance: String(entry.amount), + symbol: entry.metadata?.symbol ?? 'UNKNOWN', + name: entry.metadata?.name ?? entry.asset_type, + precision: entry.metadata?.decimals ?? 0, + }) } return { - balance, + balance: nativeBalance, chainId: this.chainId, assetId: this.assetId, chain: this.getType(), - chainSpecific: {}, + chainSpecific: { tokens }, pubkey, } } catch (err) { @@ -179,41 +219,51 @@ export class ChainAdapter implements IChainAdapter { throw new Error('Aptos transaction history not yet implemented') } + private async estimateMaxGasAmount( + sender: string, + data: InputEntryFunctionData, + ): Promise { + const tx = await this.client.transaction.build.simple({ + sender, + data, + options: { maxGasAmount: 2_000_000 }, + }) + const dummyPubKey = new Ed25519PublicKey('0x' + '00'.repeat(32)) + const [sim] = await this.client.transaction.simulate.simple({ + signerPublicKey: dummyPubKey, + transaction: tx, + options: { estimateGasUnitPrice: true, estimateMaxGasAmount: true }, + }) + const recommended = BigInt(sim?.max_gas_amount ?? sim?.gas_used ?? 0) + return recommended > MIN_MAX_GAS_AMOUNT ? recommended : MIN_MAX_GAS_AMOUNT + } + async buildSendApiTransaction( input: BuildSendApiTxInput, ): Promise> { try { const { from, accountNumber, to, value } = input - // Build a raw Aptos transaction for signing - // We construct the BCS-encoded transaction payload for 0x1::coin::transfer - const sequenceNumber = await this.getSequenceNumber(from) - const gasEstimate = await this.getGasPrice() - const chainId = 1 // Aptos mainnet chain ID + const data: InputEntryFunctionData = { + function: '0x1::aptos_account::transfer_coins', + typeArguments: [APT_COIN_TYPE], + functionArguments: [to, BigInt(value)], + } + const maxGasAmount = Number(await this.estimateMaxGasAmount(from, data)) - // Build the raw transaction structure - const txData = { + const transaction = await this.client.transaction.build.simple({ sender: from, - sequence_number: sequenceNumber, - max_gas_amount: '2000', - gas_unit_price: gasEstimate, - expiration_timestamp_secs: String(Math.floor(Date.now() / 1000) + 3600), - chain_id: chainId, - payload: { - type: 'entry_function_payload', - function: '0x1::coin::transfer', - type_arguments: ['0x1::aptos_coin::AptosCoin'], - arguments: [to, value], - }, - } + data, + options: { maxGasAmount }, + }) - // Serialize the transaction for the hardware wallet to sign - // The wallet expects raw BCS bytes via aptosSignTx - const txBytes = new TextEncoder().encode(JSON.stringify(txData)) + const signingMessageBytes = this.client.getSigningMessage({ transaction }) + const rawTransactionBytes = transaction.bcsToBytes() return { addressNList: toAddressNList(this.getBip44Params({ accountNumber })), - txBytes, + signingMessageBytes, + rawTransactionBytes, } } catch (err) { return ErrorHandler(err, { @@ -250,7 +300,8 @@ export class ChainAdapter implements IChainAdapter { const signedTx = await wallet.aptosSignTx({ addressNList: txToSign.addressNList, - txBytes: txToSign.txBytes, + signingMessageBytes: txToSign.signingMessageBytes, + rawTransactionBytes: txToSign.rawTransactionBytes, }) if (!signedTx?.signature || !signedTx?.publicKey) { @@ -260,7 +311,7 @@ export class ChainAdapter implements IChainAdapter { return JSON.stringify({ signature: signedTx.signature, publicKey: signedTx.publicKey, - txBytes: Array.from(signedTx.txBytes), + rawTransactionBytes: Array.from(signedTx.rawTransactionBytes), }) } catch (err) { return ErrorHandler(err, { @@ -270,11 +321,13 @@ export class ChainAdapter implements IChainAdapter { } async signAndBroadcastTransaction({ + senderAddress, + receiverAddress, signTxInput, }: SignAndBroadcastTransactionInput): Promise { try { const signedTxHex = await this.signTransaction(signTxInput) - return this.broadcastTransaction({ hex: signedTxHex }) + return this.broadcastTransaction({ senderAddress, receiverAddress, hex: signedTxHex }) } catch (err) { return ErrorHandler(err, { translation: 'chainAdapters.errors.signAndBroadcastTransaction', @@ -285,31 +338,25 @@ export class ChainAdapter implements IChainAdapter { async broadcastTransaction(input: BroadcastTransactionInput): Promise { try { const { hex } = input - const parsed = JSON.parse(hex) - - // Submit the signed transaction to the Aptos REST API - // The signed transaction needs to be submitted as a BCS-encoded body - const response = await fetch(`${this.rpcUrl}/transactions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - sender: `0x${parsed.publicKey}`, - signature: { - type: 'ed25519_signature', - public_key: `0x${parsed.publicKey}`, - signature: `0x${parsed.signature}`, - }, - payload: JSON.parse(new TextDecoder().decode(new Uint8Array(parsed.txBytes))).payload, - }), - }) - - if (!response.ok) { - const errorBody = await response.text() - throw new Error(`Aptos broadcast failed: ${response.status} - ${errorBody}`) + const parsed = JSON.parse(hex) as { + signature: string + publicKey: string + rawTransactionBytes: number[] } - const result = await response.json() - return result.hash ?? result.version?.toString() ?? '' + const rawBytes = new Uint8Array(parsed.rawTransactionBytes) + const transaction = SimpleTransaction.deserialize(new Deserializer(rawBytes)) + + const publicKey = new Ed25519PublicKey(parsed.publicKey) + const signature = new Ed25519Signature(parsed.signature) + const senderAuthenticator = new AccountAuthenticatorEd25519(publicKey, signature) + + const pending = await this.client.transaction.submit.simple({ + transaction, + senderAuthenticator, + }) + + return pending.hash } catch (err) { return ErrorHandler(err, { translation: 'chainAdapters.errors.broadcastTransaction', @@ -321,20 +368,54 @@ export class ChainAdapter implements IChainAdapter { input: GetFeeDataInput, ): Promise> { try { - const gasPrice = await this.getGasPrice() - const maxGasAmount = '2000' - const txFee = (BigInt(maxGasAmount) * BigInt(gasPrice)).toString() - - const feeData = { - gasEstimate: txFee, - gasUnitPrice: gasPrice, - maxGasAmount, + const { chainSpecific } = input + const { from } = chainSpecific + + const { gas_estimate, prioritized_gas_estimate, deprioritized_gas_estimate } = + await this.client.getGasPriceEstimation() + + let maxGasAmount: string + try { + const estimate = await this.estimateMaxGasAmount(from, { + function: '0x1::aptos_account::transfer_coins', + typeArguments: [APT_COIN_TYPE], + functionArguments: [from, 0n], + }) + maxGasAmount = estimate.toString() + } catch { + maxGasAmount = MIN_MAX_GAS_AMOUNT.toString() } + const slowPrice = String(deprioritized_gas_estimate ?? gas_estimate ?? 100) + const averagePrice = String(gas_estimate ?? 100) + const fastPrice = String(prioritized_gas_estimate ?? gas_estimate ?? 100) + const calcTxFee = (price: string) => (BigInt(maxGasAmount) * BigInt(price)).toString() + return { - fast: { txFee, chainSpecific: feeData }, - average: { txFee, chainSpecific: feeData }, - slow: { txFee, chainSpecific: feeData }, + fast: { + txFee: calcTxFee(fastPrice), + chainSpecific: { + gasEstimate: calcTxFee(fastPrice), + gasUnitPrice: fastPrice, + maxGasAmount, + }, + }, + average: { + txFee: calcTxFee(averagePrice), + chainSpecific: { + gasEstimate: calcTxFee(averagePrice), + gasUnitPrice: averagePrice, + maxGasAmount, + }, + }, + slow: { + txFee: calcTxFee(slowPrice), + chainSpecific: { + gasEstimate: calcTxFee(slowPrice), + gasUnitPrice: slowPrice, + maxGasAmount, + }, + }, } } catch (err) { return ErrorHandler(err, { @@ -355,23 +436,65 @@ export class ChainAdapter implements IChainAdapter { return } + private getPayloadAssetId(payload: { + function?: string + type_arguments?: string[] + arguments?: unknown[] + }): AssetId | undefined { + const fn = payload?.function + if (!fn) return undefined + + if (fn === '0x1::coin::transfer' || fn === '0x1::aptos_account::transfer_coins') { + const coinType = payload.type_arguments?.[0] + if (!coinType) return undefined + if (coinType === APT_COIN_TYPE) return this.assetId + return toAssetId({ + chainId: this.chainId, + assetNamespace: ASSET_NAMESPACE.aptosCoin, + assetReference: coinType, + }) + } + + if (fn === '0x1::primary_fungible_store::transfer') { + const metadata = payload.arguments?.[0] + const ref = typeof metadata === 'string' ? metadata : (metadata as { inner?: string })?.inner + if (!ref) return undefined + return toAssetId({ + chainId: this.chainId, + assetNamespace: ASSET_NAMESPACE.aptosCoin, + assetReference: ref, + }) + } + + if (fn === '0x1::aptos_account::transfer') { + return this.assetId + } + + return undefined + } + async parseTx(txHashOrTx: unknown, pubkey: string): Promise { try { - const txHash = typeof txHashOrTx === 'string' ? txHashOrTx : '' - - const response = await fetch(`${this.rpcUrl}/transactions/by_hash/${txHash}`) - if (!response.ok) { - throw new Error(`Aptos tx lookup failed: ${response.status}`) + const tx = ( + typeof txHashOrTx === 'string' + ? await this.client.transaction.getTransactionByHash({ transactionHash: txHashOrTx }) + : txHashOrTx + ) as { + hash?: string + version?: string | number + timestamp?: string | number + success?: boolean + gas_used?: string + gas_unit_price?: string + sender?: string + payload?: { function?: string; type_arguments?: string[]; arguments?: unknown[] } } - const tx = await response.json() - - const txid = tx.hash ?? txHash + const txid = tx.hash ?? (typeof txHashOrTx === 'string' ? txHashOrTx : '') const blockHeight = Number(tx.version ?? 0) - const blockTime = tx.timestamp ? Math.floor(Number(tx.timestamp) / 1000) : 0 + const blockTime = tx.timestamp ? Math.floor(Number(tx.timestamp) / 1_000_000) : 0 - const success = tx.success !== false - const status = success ? TxStatus.Confirmed : TxStatus.Failed + const status = tx.success === false ? TxStatus.Failed : TxStatus.Confirmed const gasUsed = tx.gas_used ?? '0' const gasUnitPrice = tx.gas_unit_price ?? '0' @@ -381,67 +504,33 @@ export class ChainAdapter implements IChainAdapter { } const transfers: Transaction['transfers'] = [] - - // Parse events for transfers - const events = tx.events ?? [] - for (const event of events) { - if (event.type === '0x1::coin::WithdrawEvent' || event.type === '0x1::coin::DepositEvent') { - // These are coin module events, skip in favor of ChangeEvent - continue - } - - if (event.type?.includes('::CoinTransfer') || event.type?.includes('::Withdraw') || event.type?.includes('::Deposit')) { - const amount = event.data?.amount ?? event.data?.value ?? '0' - const isFromSender = event.guid?.account_address === pubkey || event.data?.from === pubkey - const isToSender = event.data?.to === pubkey - - if (isFromSender) { - transfers.push({ - assetId: this.assetId, - from: [event.data?.from ?? event.guid?.account_address ?? pubkey], - to: [event.data?.to ?? ''], - type: TransferType.Send, - value: amount, - }) - } - if (isToSender) { - transfers.push({ - assetId: this.assetId, - from: [event.data?.from ?? ''], - to: [event.data?.to ?? pubkey], - type: TransferType.Receive, - value: amount, - }) - } - } - } - - // If no transfers found from events, try to parse from the payload - if (transfers.length === 0 && tx.payload?.function === '0x1::coin::transfer') { - const args = tx.payload.arguments ?? [] - const recipient = args[0] ?? '' - const amount = args[1] ?? '0' - const sender = tx.sender ?? pubkey - - const isSend = sender === pubkey - const isReceive = recipient === pubkey - - if (isSend) { + const payload = tx.payload ?? {} + const transferAssetId = this.getPayloadAssetId(payload) + + if (transferAssetId) { + const args = payload.arguments ?? [] + const fn = payload.function ?? '' + const isFaTransfer = fn === '0x1::primary_fungible_store::transfer' + const recipient = String(args[isFaTransfer ? 1 : 0] ?? '') + const amount = String(args[isFaTransfer ? 2 : 1] ?? '0') + const sender = tx.sender ?? '' + + if (sender === pubkey) { transfers.push({ - assetId: this.assetId, + assetId: transferAssetId, from: [sender], to: [recipient], type: TransferType.Send, - value: String(amount), + value: amount, }) } - if (isReceive) { + if (recipient === pubkey) { transfers.push({ - assetId: this.assetId, + assetId: transferAssetId, from: [sender], to: [recipient], type: TransferType.Receive, - value: String(amount), + value: amount, }) } } @@ -464,26 +553,4 @@ export class ChainAdapter implements IChainAdapter { }) } } - - private async getSequenceNumber(address: string): Promise { - try { - const response = await fetch(`${this.rpcUrl}/accounts/${address}`) - if (!response.ok) return '0' - const data = await response.json() - return data.sequence_number ?? '0' - } catch { - return '0' - } - } - - private async getGasPrice(): Promise { - try { - const response = await fetch(`${this.rpcUrl}/transactions/estimate_gas_price`) - if (!response.ok) return '100' - const data = await response.json() - return data.gas_estimate?.toString() ?? '100' - } catch { - return '100' - } - } } diff --git a/packages/chain-adapters/src/types.ts b/packages/chain-adapters/src/types.ts index 3b2692b39c8..5859d105a35 100644 --- a/packages/chain-adapters/src/types.ts +++ b/packages/chain-adapters/src/types.ts @@ -20,13 +20,13 @@ import type { import type * as unchained from '@shapeshiftoss/unchained-client' import type PQueue from 'p-queue' +import type * as aptos from './aptos/types' import type * as cosmossdk from './cosmossdk/types' import type * as evm from './evm/types' import type * as near from './near/types' import type * as solana from './solana/types' import type * as starknet from './starknet/types' import type * as sui from './sui/types' -import type * as aptos from './aptos/types' import type * as ton from './ton/types' import type * as tron from './tron/types' import type * as utxo from './utxo/types' diff --git a/packages/hdwallet-core/src/aptos.ts b/packages/hdwallet-core/src/aptos.ts index d919802d762..9af672c1eab 100644 --- a/packages/hdwallet-core/src/aptos.ts +++ b/packages/hdwallet-core/src/aptos.ts @@ -8,14 +8,16 @@ export interface AptosGetAddress { export interface AptosSignTx { addressNList: BIP32Path - /** Raw transaction bytes to sign (BCS serialized) */ - txBytes: Uint8Array + /** Signing message bytes (sha3-256 prefix + BCS raw transaction) for off-device signers */ + signingMessageBytes: Uint8Array + /** BCS-serialized SimpleTransaction for on-device signers and broadcast reconstruction */ + rawTransactionBytes: Uint8Array } export interface AptosSignedTx { signature: string publicKey: string - txBytes: Uint8Array + rawTransactionBytes: Uint8Array } export interface AptosGetAccountPaths { @@ -108,12 +110,6 @@ export function aptosNextAccountPath(msg: AptosAccountPath): AptosAccountPath | const nextIndex = (msg.addressNList[4] & 0x7fffffff) + 1 return { - addressNList: [ - 0x80000000 + 44, - 0x80000000 + slip44, - 0x80000000 + 0, - 0x80000000 + 0, - nextIndex, - ], + addressNList: [0x80000000 + 44, 0x80000000 + slip44, 0x80000000 + 0, 0x80000000 + 0, nextIndex], } } diff --git a/packages/hdwallet-core/src/wallet.ts b/packages/hdwallet-core/src/wallet.ts index b4827662131..8045119c6b7 100644 --- a/packages/hdwallet-core/src/wallet.ts +++ b/packages/hdwallet-core/src/wallet.ts @@ -1,5 +1,6 @@ import isObject from 'lodash/isObject' +import type { AptosWallet, AptosWalletInfo } from './aptos' import type { ArkeoWallet, ArkeoWalletInfo } from './arkeo' import type { BinanceWallet, BinanceWalletInfo } from './binance' import type { BTCInputScriptType, BTCWallet, BTCWalletInfo } from './bitcoin' @@ -18,7 +19,6 @@ import type { SuiWallet, SuiWalletInfo } from './sui' import type { TerraWallet, TerraWalletInfo } from './terra' import type { ThorchainWallet, ThorchainWalletInfo } from './thorchain' import type { TonWallet, TonWalletInfo } from './ton' -import type { AptosWallet, AptosWalletInfo } from './aptos' import type { Transport } from './transport' import type { TronWallet, TronWalletInfo } from './tron' @@ -402,6 +402,10 @@ export function infoTon(info: HDWalletInfo): info is TonWalletInfo { return isObject(info) && (info as any)._supportsTonInfo } +export function supportsDebugLink(wallet: HDWallet): wallet is DebugLinkWallet { + return isObject(wallet) && (wallet as any)._supportsDebugLink +} + export function supportsAptos(wallet: HDWallet): wallet is AptosWallet { return isObject(wallet) && (wallet as any)._supportsAptos } diff --git a/packages/hdwallet-native/src/aptos.ts b/packages/hdwallet-native/src/aptos.ts index ac5ca6d4a8d..81261acacf6 100644 --- a/packages/hdwallet-native/src/aptos.ts +++ b/packages/hdwallet-native/src/aptos.ts @@ -49,14 +49,14 @@ export function MixinNativeAptosWallet { return this.needsMnemonic(!!this.aptosAdapter, async () => { const { signature, publicKey } = await this.aptosAdapter!.signTransaction( - msg.txBytes, + msg.signingMessageBytes, msg.addressNList, ) return { signature, publicKey, - txBytes: msg.txBytes, + rawTransactionBytes: msg.rawTransactionBytes, } }) } diff --git a/packages/hdwallet-native/src/crypto/isolation/adapters/aptos.ts b/packages/hdwallet-native/src/crypto/isolation/adapters/aptos.ts index b5010999fe6..b575328d051 100644 --- a/packages/hdwallet-native/src/crypto/isolation/adapters/aptos.ts +++ b/packages/hdwallet-native/src/crypto/isolation/adapters/aptos.ts @@ -1,6 +1,7 @@ import * as core from '@shapeshiftoss/hdwallet-core' +import { createSHA3 } from 'hash-wasm' -import { Isolation } from './crypto' +import type { Isolation } from '../..' const ED25519_PUBLIC_KEY_SIZE = 32 @@ -19,11 +20,15 @@ export class AptosAdapter { async getAddress(addressNList: core.BIP32Path): Promise { const publicKey = await this.getPublicKeyRaw(addressNList) - // Aptos address = SHA3-256 of the public key, prefixed with 0x - // For single-key accounts, the address is 0x + sha3_256(publicKey) - const { createHash } = await import('crypto') - const hash = createHash('sha3-256').update(publicKey).digest('hex') - return `0x${hash}` + const SIGNATURE_SCHEME_FLAG_ED25519 = 0x00 + const flaggedPublicKey = new Uint8Array(publicKey.length + 1) + flaggedPublicKey.set(publicKey, 0) + flaggedPublicKey[publicKey.length] = SIGNATURE_SCHEME_FLAG_ED25519 + + const sha3 = await createSHA3(256) + sha3.init() + sha3.update(flaggedPublicKey) + return `0x${sha3.digest('hex')}` } async getPublicKey(addressNList: core.BIP32Path): Promise { diff --git a/packages/hdwallet-native/src/native.ts b/packages/hdwallet-native/src/native.ts index c1695f4813c..09f94a22417 100644 --- a/packages/hdwallet-native/src/native.ts +++ b/packages/hdwallet-native/src/native.ts @@ -5,6 +5,7 @@ import * as eventemitter2 from 'eventemitter2' import isObject from 'lodash/isObject' import type { NativeAdapterArgs } from './adapter' +import { MixinNativeAptosWallet, MixinNativeAptosWalletInfo } from './aptos' import { MixinNativeArkeoWallet, MixinNativeArkeoWalletInfo } from './arkeo' import { MixinNativeBTCWallet, MixinNativeBTCWalletInfo } from './bitcoin' import { MixinNativeCosmosWallet, MixinNativeCosmosWalletInfo } from './cosmos' @@ -22,7 +23,6 @@ import { MixinNativeSuiWallet, MixinNativeSuiWalletInfo } from './sui' import { MixinNativeTerraWallet, MixinNativeTerraWalletInfo } from './terra' import { MixinNativeThorchainWallet, MixinNativeThorchainWalletInfo } from './thorchain' import { MixinNativeTonWallet, MixinNativeTonWalletInfo } from './ton' -import { MixinNativeAptosWallet, MixinNativeAptosWalletInfo } from './aptos' import { MixinNativeTronWallet, MixinNativeTronWalletInfo } from './tron' export { NativeEvents } from './nativeEvents' @@ -135,15 +135,16 @@ class NativeHDWalletInfo MixinNativeTronWalletInfo( MixinNativeTonWalletInfo( MixinNativeAptosWalletInfo( - MixinNativeSuiWalletInfo( - MixinNativeNearWalletInfo( - MixinNativeThorchainWalletInfo( - MixinNativeMayachainWalletInfo( - MixinNativeSecretWalletInfo( - MixinNativeTerraWalletInfo( - MixinNativeKavaWalletInfo( - MixinNativeArkeoWalletInfo( - MixinNativeOsmosisWalletInfo(NativeHDWalletBase), + MixinNativeSuiWalletInfo( + MixinNativeNearWalletInfo( + MixinNativeThorchainWalletInfo( + MixinNativeMayachainWalletInfo( + MixinNativeSecretWalletInfo( + MixinNativeTerraWalletInfo( + MixinNativeKavaWalletInfo( + MixinNativeArkeoWalletInfo( + MixinNativeOsmosisWalletInfo(NativeHDWalletBase), + ), ), ), ), @@ -168,7 +169,6 @@ class NativeHDWalletInfo core.StarknetWalletInfo, core.TronWalletInfo, core.TonWalletInfo, - core.AptosWalletInfo, core.SuiWalletInfo, core.NearWalletInfo, core.ThorchainWalletInfo, @@ -177,7 +177,8 @@ class NativeHDWalletInfo core.TerraWalletInfo, core.KavaWalletInfo, core.ArkeoWalletInfo, - core.OsmosisWalletInfo + core.OsmosisWalletInfo, + core.AptosWalletInfo { describePath(msg: core.DescribePath): core.PathDescription { switch (msg.coin.toLowerCase()) { @@ -252,14 +253,17 @@ export class NativeHDWallet MixinNativeTronWallet( MixinNativeTonWallet( MixinNativeAptosWallet( - MixinNativeSuiWallet( - MixinNativeNearWallet( - MixinNativeThorchainWallet( - MixinNativeMayachainWallet( - MixinNativeSecretWallet( - MixinNativeTerraWallet( - MixinNativeKavaWallet( - MixinNativeOsmosisWallet(MixinNativeArkeoWallet(NativeHDWalletInfo)), + MixinNativeSuiWallet( + MixinNativeNearWallet( + MixinNativeThorchainWallet( + MixinNativeMayachainWallet( + MixinNativeSecretWallet( + MixinNativeTerraWallet( + MixinNativeKavaWallet( + MixinNativeOsmosisWallet( + MixinNativeArkeoWallet(NativeHDWalletInfo), + ), + ), ), ), ), @@ -283,7 +287,6 @@ export class NativeHDWallet core.StarknetWallet, core.TronWallet, core.TonWallet, - core.AptosWallet, core.SuiWallet, core.NearWallet, core.ThorchainWallet, @@ -292,7 +295,8 @@ export class NativeHDWallet core.TerraWallet, core.KavaWallet, core.OsmosisWallet, - core.ArkeoWallet + core.ArkeoWallet, + core.AptosWallet { readonly _isNative = true @@ -513,7 +517,6 @@ export class NativeHDWallet super.suiWipe() super.nearWipe() super.tonWipe() - super.aptosWipe() super.btcWipe() super.ethWipe() super.cosmosWipe() @@ -526,6 +529,7 @@ export class NativeHDWallet super.terraWipe() super.kavaWipe() super.arkeoWipe() + super.aptosWipe() ;(await oldSecp256k1MasterKey)?.revoke?.() ;(await oldEd25519MasterKey)?.revoke?.() } diff --git a/packages/public-api/src/swapperDeps.ts b/packages/public-api/src/swapperDeps.ts index 67e89c2367d..29537ca4baa 100644 --- a/packages/public-api/src/swapperDeps.ts +++ b/packages/public-api/src/swapperDeps.ts @@ -321,5 +321,8 @@ export const getSwapperDeps = (): SwapperDeps => ({ assertGetNearChainAdapter: () => nearAdapter, assertGetStarknetChainAdapter: () => starknetAdapter, assertGetTonChainAdapter: () => tonAdapter, + assertGetAptosChainAdapter: () => { + throw new Error('No Aptos chain adapter registered in public-api') + }, fetchIsSmartContractAddressQuery: () => Promise.resolve(false), }) diff --git a/packages/swapper/package.json b/packages/swapper/package.json index a3802d1af4f..3548d0940aa 100644 --- a/packages/swapper/package.json +++ b/packages/swapper/package.json @@ -29,6 +29,7 @@ "postbuild:cjs": "echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json" }, "dependencies": { + "@aptos-labs/ts-sdk": "6.3.1", "@arbitrum/sdk": "^4.0.1", "@avnu/avnu-sdk": "^4.0.1", "@cetusprotocol/aggregator-sdk": "^1.4.2", diff --git a/packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.test.ts b/packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.test.ts index c569c883c90..547a4d1afc7 100644 --- a/packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.test.ts +++ b/packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.test.ts @@ -43,6 +43,7 @@ describe('getTradeQuote', () => { assertGetNearChainAdapter: () => vi.fn() as any, assertGetStarknetChainAdapter: () => vi.fn() as any, assertGetTonChainAdapter: () => vi.fn() as any, + assertGetAptosChainAdapter: () => vi.fn() as any, config: { VITE_BUTTERSWAP_CLIENT_ID: 'test', } as any, diff --git a/packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeRate.test.ts b/packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeRate.test.ts index d875ab5932c..7c86f782cc2 100644 --- a/packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeRate.test.ts +++ b/packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeRate.test.ts @@ -32,6 +32,7 @@ describe('getTradeRate', () => { assertGetNearChainAdapter: () => vi.fn() as any, assertGetStarknetChainAdapter: () => vi.fn() as any, assertGetTonChainAdapter: () => vi.fn() as any, + assertGetAptosChainAdapter: () => vi.fn() as any, config: { VITE_BUTTERSWAP_CLIENT_ID: 'test', } as any, @@ -76,6 +77,7 @@ describe('getTradeRate', () => { assertGetNearChainAdapter: () => vi.fn() as any, assertGetStarknetChainAdapter: () => vi.fn() as any, assertGetTonChainAdapter: () => vi.fn() as any, + assertGetAptosChainAdapter: () => vi.fn() as any, config: { VITE_BUTTERSWAP_CLIENT_ID: 'test', } as any, diff --git a/packages/swapper/src/swappers/NearIntentsSwapper/NearIntentsSwapper.ts b/packages/swapper/src/swappers/NearIntentsSwapper/NearIntentsSwapper.ts index 1ab787cdc3b..de5c7cd0abc 100644 --- a/packages/swapper/src/swappers/NearIntentsSwapper/NearIntentsSwapper.ts +++ b/packages/swapper/src/swappers/NearIntentsSwapper/NearIntentsSwapper.ts @@ -1,5 +1,6 @@ import type { Swapper } from '../../types' import { + executeAptosTransaction, executeEvmTransaction, executeNearTransaction, executeSolanaTransaction, @@ -17,6 +18,7 @@ export const nearIntentsSwapper: Swapper = { executeSuiTransaction, executeNearTransaction, executeTonTransaction, + executeAptosTransaction, executeUtxoTransaction: (txToSign, { signAndBroadcastTransaction }) => { return signAndBroadcastTransaction(txToSign) }, diff --git a/packages/swapper/src/swappers/NearIntentsSwapper/endpoints.ts b/packages/swapper/src/swappers/NearIntentsSwapper/endpoints.ts index f2944275529..bbe428fc1eb 100644 --- a/packages/swapper/src/swappers/NearIntentsSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/NearIntentsSwapper/endpoints.ts @@ -5,6 +5,7 @@ import { contractAddressOrUndefined } from '@shapeshiftoss/utils' import { getTronTransactionFees } from '../../tron-utils/getTronTransactionFees' import { getUnsignedTronTransaction } from '../../tron-utils/getUnsignedTronTransaction' import type { + GetUnsignedAptosTransactionArgs, GetUnsignedNearTransactionArgs, GetUnsignedSuiTransactionArgs, GetUnsignedTonTransactionArgs, @@ -377,6 +378,43 @@ export const nearIntentsApi: SwapperApi = { return Promise.resolve(step.feeData.networkFeeCryptoBaseUnit) }, + getUnsignedAptosTransaction: ({ + stepIndex, + tradeQuote, + from, + assertGetAptosChainAdapter, + }: GetUnsignedAptosTransactionArgs) => { + if (!isExecutableTradeQuote(tradeQuote)) throw new Error('Unable to execute a trade rate quote') + + const step = getExecutableTradeStep(tradeQuote, stepIndex) + + const { accountNumber, sellAsset, nearIntentsSpecific } = step + if (!nearIntentsSpecific) throw new Error('nearIntentsSpecific is required') + + const adapter = assertGetAptosChainAdapter(sellAsset.chainId) + + const to = nearIntentsSpecific.depositAddress + const value = step.sellAmountIncludingProtocolFeesCryptoBaseUnit + + return adapter.buildSendApiTransaction({ + to, + from, + value, + accountNumber, + chainSpecific: {}, + }) + }, + + getAptosTransactionFees: ({ tradeQuote, stepIndex }: GetUnsignedAptosTransactionArgs) => { + if (!isExecutableTradeQuote(tradeQuote)) throw new Error('Unable to execute a trade rate quote') + + const step = getExecutableTradeStep(tradeQuote, stepIndex) + if (!step.feeData.networkFeeCryptoBaseUnit) { + throw new Error('Missing network fee in quote') + } + return Promise.resolve(step.feeData.networkFeeCryptoBaseUnit) + }, + checkTradeStatus: async ({ config, swap }): Promise => { const { nearIntentsSpecific } = swap?.metadata ?? {} diff --git a/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeQuote.ts index fc6e74c8781..6575ade92d9 100644 --- a/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeQuote.ts @@ -332,6 +332,17 @@ export const getTradeQuote = async ( return { networkFeeCryptoBaseUnit: feeData.fast.txFee } } + case CHAIN_NAMESPACE.Aptos: { + const sellAdapter = deps.assertGetAptosChainAdapter(sellAsset.chainId) + const feeData = await sellAdapter.getFeeData({ + to: depositAddress, + value: sellAmount, + chainSpecific: { from }, + }) + + return { networkFeeCryptoBaseUnit: feeData.fast.txFee } + } + default: throw new Error(`Unsupported chain namespace: ${chainNamespace}`) } diff --git a/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeRate.ts index 33ca21676c4..d080d1fd3d7 100644 --- a/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeRate.ts +++ b/packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeRate.ts @@ -381,6 +381,26 @@ export const getTradeRate = async ( } } + case CHAIN_NAMESPACE.Aptos: { + try { + const sellAdapter = deps.assertGetAptosChainAdapter(sellAsset.chainId) + + if (!sendAddress) { + return '0' + } + + const feeData = await sellAdapter.getFeeData({ + to: depositAddress, + value: sellAmount, + chainSpecific: { from: sendAddress }, + }) + + return feeData.fast.txFee + } catch (error) { + return '0' + } + } + default: return undefined } diff --git a/packages/swapper/src/swappers/NearIntentsSwapper/types.ts b/packages/swapper/src/swappers/NearIntentsSwapper/types.ts index 22907ce6d16..05d2b87b171 100644 --- a/packages/swapper/src/swappers/NearIntentsSwapper/types.ts +++ b/packages/swapper/src/swappers/NearIntentsSwapper/types.ts @@ -27,6 +27,7 @@ export const nearIntentsSupportedChainIds = [ KnownChainIds.NearMainnet, KnownChainIds.PlasmaMainnet, KnownChainIds.TonMainnet, + KnownChainIds.AptosMainnet, ] as const export type NearIntentsSupportedChainId = (typeof nearIntentsSupportedChainIds)[number] @@ -52,4 +53,5 @@ export const chainIdToNearIntentsChain: Record = { [KnownChainIds.MonadMainnet]: DAO_TREASURY_MONAD, [KnownChainIds.HyperEvmMainnet]: DAO_TREASURY_HYPEREVM, [KnownChainIds.BobMainnet]: DAO_TREASURY_BOB, + [KnownChainIds.AptosMainnet]: DAO_TREASURY_APTOS, } export const getTreasuryAddressFromChainId = (chainId: ChainId): string => { diff --git a/packages/swapper/src/swappers/utils/test-data/cryptoMarketDataById.ts b/packages/swapper/src/swappers/utils/test-data/cryptoMarketDataById.ts index a065efeee70..5809188acc9 100644 --- a/packages/swapper/src/swappers/utils/test-data/cryptoMarketDataById.ts +++ b/packages/swapper/src/swappers/utils/test-data/cryptoMarketDataById.ts @@ -1,3 +1,5 @@ +import type { AssetId } from '@shapeshiftoss/caip' + import { AVAX, BSC, @@ -13,7 +15,7 @@ import { WETH, } from './assets' -export const marketDataByAssetIdUsd = { +export const marketDataByAssetIdUsd: Record = { [FOX_MAINNET.assetId]: { price: '0.04' }, [FOX_GNOSIS.assetId]: { price: '0.04' }, [ETH.assetId]: { price: '1300' }, diff --git a/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts b/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts index b090299cc3c..480d4f059c1 100644 --- a/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts +++ b/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts @@ -669,6 +669,14 @@ export const getL1RateOrQuote = async ( }), ) } + case CHAIN_NAMESPACE.Aptos: { + return Err( + makeSwapErrorRight({ + message: 'Aptos is not supported', + code: TradeQuoteError.UnsupportedTradePair, + }), + ) + } default: return assertUnreachable(chainNamespace) } diff --git a/packages/swapper/src/utils.ts b/packages/swapper/src/utils.ts index a3fbc967194..69875a56440 100644 --- a/packages/swapper/src/utils.ts +++ b/packages/swapper/src/utils.ts @@ -1,6 +1,7 @@ import type { AssetId, ChainId } from '@shapeshiftoss/caip' -import { solanaChainId, starknetChainId, suiChainId } from '@shapeshiftoss/caip' +import { aptosChainId, solanaChainId, starknetChainId, suiChainId } from '@shapeshiftoss/caip' import type { + aptos, EvmChainAdapter, near, SignTx, @@ -11,9 +12,14 @@ import type { } from '@shapeshiftoss/chain-adapters' import { isSecondClassEvmAdapter } from '@shapeshiftoss/chain-adapters' import type { TronSignTx } from '@shapeshiftoss/chain-adapters/src/tron/types' -import type { AptosSignTx, SolanaSignTx, StarknetSignTx, SuiSignTx } from '@shapeshiftoss/hdwallet-core' +import type { + AptosSignTx, + SolanaSignTx, + StarknetSignTx, + SuiSignTx, +} from '@shapeshiftoss/hdwallet-core' import type { Asset, EvmChainId } from '@shapeshiftoss/types' -import { evm, TxStatus } from '@shapeshiftoss/unchained-client' +import { evm, TransferType, TxStatus } from '@shapeshiftoss/unchained-client' import { BigAmount, bn } from '@shapeshiftoss/utils' import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' @@ -511,6 +517,38 @@ export const checkStarknetSwapStatus = async ({ } } +export const checkAptosSwapStatus = async ({ + txHash, + address, + assertGetAptosChainAdapter, +}: { + txHash: string + address: string | undefined + assertGetAptosChainAdapter: (chainId: ChainId) => aptos.ChainAdapter +}): Promise => { + try { + if (!address) throw new Error('Missing address') + + const adapter = assertGetAptosChainAdapter(aptosChainId) + const tx = await adapter.parseTx(txHash, address) + + const receiveTransfer = tx.transfers.find( + t => t.type === TransferType.Receive && t.to.includes(address), + ) + const actualBuyAmountCryptoBaseUnit = receiveTransfer?.value + + return { + status: tx.status, + buyTxHash: txHash, + message: undefined, + actualBuyAmountCryptoBaseUnit, + } + } catch (e) { + console.error(e) + return createDefaultStatusResponse(txHash) + } +} + export class SolanaLogsError extends Error { constructor(name: string) { super(name) diff --git a/packages/utils/src/assetData/baseAssets.ts b/packages/utils/src/assetData/baseAssets.ts index eb3b022989a..6f888b165ef 100644 --- a/packages/utils/src/assetData/baseAssets.ts +++ b/packages/utils/src/assetData/baseAssets.ts @@ -867,7 +867,7 @@ export const aptos: Readonly = Object.freeze({ precision: 8, color: '#2CD5E5', networkColor: '#2CD5E5', - icon: 'https://assets.coingecko.com/coins/images/25788/large/aptos.png?1696520069', + icon: 'https://rawcdn.githack.com/trustwallet/assets/master/blockchains/aptos/info/logo.png', explorer: 'https://explorer.aptoslabs.com', explorerAddressLink: 'https://explorer.aptoslabs.com/account/', explorerTxLink: 'https://explorer.aptoslabs.com/txn/', diff --git a/packages/utils/src/assetData/getBaseAsset.ts b/packages/utils/src/assetData/getBaseAsset.ts index d4ef05109be..4c706217e30 100644 --- a/packages/utils/src/assetData/getBaseAsset.ts +++ b/packages/utils/src/assetData/getBaseAsset.ts @@ -4,8 +4,8 @@ import { KnownChainIds } from '@shapeshiftoss/types' import { assertUnreachable } from '../assertUnreachable' import { - aptos, abstract, + aptos, arbitrum, atom, avax, diff --git a/packages/utils/src/chainIdToFeeAssetId.ts b/packages/utils/src/chainIdToFeeAssetId.ts index 8fba94b0102..e1b1c6fb490 100644 --- a/packages/utils/src/chainIdToFeeAssetId.ts +++ b/packages/utils/src/chainIdToFeeAssetId.ts @@ -1,7 +1,7 @@ import type { AssetId, ChainId } from '@shapeshiftoss/caip' import { - aptosAssetId, abstractAssetId, + aptosAssetId, arbitrumAssetId, avalancheAssetId, baseAssetId, diff --git a/packages/utils/src/getNativeFeeAssetReference.ts b/packages/utils/src/getNativeFeeAssetReference.ts index 53862e88e3d..7bb09afd7c9 100644 --- a/packages/utils/src/getNativeFeeAssetReference.ts +++ b/packages/utils/src/getNativeFeeAssetReference.ts @@ -150,6 +150,13 @@ export const getNativeFeeAssetReference = ( default: throw new Error(`Chain namespace ${chainNamespace} on ${chainReference} not supported.`) } + case CHAIN_NAMESPACE.Aptos: + switch (chainReference) { + case CHAIN_REFERENCE.AptosMainnet: + return ASSET_REFERENCE.Aptos + default: + throw new Error(`Chain namespace ${chainNamespace} on ${chainReference} not supported.`) + } default: throw new Error(`Chain namespace ${chainNamespace} on ${chainReference} not supported.`) } diff --git a/packages/utils/src/treasury.ts b/packages/utils/src/treasury.ts index 66d65bf8db9..0d32d19b861 100644 --- a/packages/utils/src/treasury.ts +++ b/packages/utils/src/treasury.ts @@ -20,6 +20,7 @@ export const treasuryChainIds = [ KnownChainIds.MonadMainnet, KnownChainIds.HyperEvmMainnet, KnownChainIds.BobMainnet, + KnownChainIds.AptosMainnet, ] as const export type TreasuryChainId = (typeof treasuryChainIds)[number] @@ -47,3 +48,7 @@ export const DAO_TREASURY_BOB = '0xF5AA59151bE6515C4Ca68A0282CF68B3eA4846fC' export const DAO_TREASURY_STARKNET = '0x07ac2252f2da7cbf085e7a5ddc1318243aa818607cdd430dd2e17dd5d487606a' export const DAO_TREASURY_TON = 'UQAHHeOhXst-zSGGigQ8KgDzz89nACBR4TxXwXNjU4DsriLb' +// TODO: replace with the real ShapeShift DAO Aptos multisig address before shipping to prod. +// See https://forum.shapeshift.com/thread/dao-treasuries-and-multisigs-43646 +export const DAO_TREASURY_APTOS = + '0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddead' diff --git a/scripts/generateAssetData/aptos/index.ts b/scripts/generateAssetData/aptos/index.ts new file mode 100644 index 00000000000..115a08c33f9 --- /dev/null +++ b/scripts/generateAssetData/aptos/index.ts @@ -0,0 +1,81 @@ +import { aptosChainId, ASSET_NAMESPACE, toAssetId } from '@shapeshiftoss/caip' +import type { Asset } from '@shapeshiftoss/types' +import { aptos, unfreeze } from '@shapeshiftoss/utils' +import axios from 'axios' +import uniqBy from 'lodash/uniqBy' + +const PANORA_API_BASE = 'https://api.panora.exchange' + +// Aptos has no public CoinGecko token list (their tokens endpoint returns 403), +// so we use Panora's tokenlist as the source of truth for tradeable assets. +// Panora is the DEX aggregator we integrate with, so this matches what users can swap. + +type PanoraToken = { + chainId: number + tokenAddress: string | null + faAddress: string | null + name: string + symbol: string + decimals: number + logoUrl: string | null + panoraUI: boolean + panoraTags: string[] | null + isBanned: boolean +} + +type PanoraResponse = { status: string; data: PanoraToken[] } | PanoraToken[] + +// Trusted tag set — excludes "Emojicoin" / "Meme"-only spam. +// Per Panora's taxonomy: Native/Verified are official, Bridged is canonical cross-chain +// (e.g. LayerZero USDC), Recognized/RWA cover BUIDL etc. +const TRUSTED_TAGS = new Set(['Verified', 'Recognized', 'RWA', 'Bridged']) + +const fetchPanoraTokens = async (): Promise => { + const apiKey = process.env.VITE_PANORA_API_KEY + if (!apiKey) throw new Error('Missing VITE_PANORA_API_KEY - source ~/.zshrc or set it') + + const { data } = await axios.get(`${PANORA_API_BASE}/tokenlist`, { + headers: { 'x-api-key': apiKey }, + timeout: 30000, + }) + + const tokens = Array.isArray(data) ? data : data.data + + return tokens + .filter(t => t.panoraUI && !t.isBanned) + .filter(t => (t.panoraTags ?? []).some(tag => TRUSTED_TAGS.has(tag))) + .filter(t => Boolean(t.faAddress || t.tokenAddress)) + .filter(t => t.tokenAddress !== '0x1::aptos_coin::AptosCoin' && t.faAddress !== '0xa') + .map(token => { + // Prefer faAddress (modern Aptos Fungible Asset standard, universally supported by Panora). + // Falls back to tokenAddress for legacy coin-standard-only tokens. + const assetReference = (token.faAddress ?? token.tokenAddress) as string + const assetId = toAssetId({ + chainId: aptosChainId, + assetNamespace: ASSET_NAMESPACE.aptosCoin, + assetReference, + }) + const asset: Asset = { + assetId, + chainId: aptosChainId, + name: token.name, + symbol: token.symbol, + precision: token.decimals, + color: aptos.color, + icon: token.logoUrl ?? '', + explorer: aptos.explorer, + explorerAddressLink: aptos.explorerAddressLink, + explorerTxLink: aptos.explorerTxLink, + relatedAssetKey: null, + } + return asset + }) +} + +export const getAssets = async (): Promise => { + const panoraAssets = await fetchPanoraTokens() + + const allAssets = uniqBy(panoraAssets, 'assetId') + + return [unfreeze(aptos), ...allAssets] +} diff --git a/scripts/generateAssetData/coingecko.ts b/scripts/generateAssetData/coingecko.ts index 8c83dbfeab8..ad4aff4e95f 100644 --- a/scripts/generateAssetData/coingecko.ts +++ b/scripts/generateAssetData/coingecko.ts @@ -2,6 +2,7 @@ import type { ChainId } from '@shapeshiftoss/caip' import { abstractChainId, adapters, + aptosChainId, arbitrumChainId, ASSET_NAMESPACE, avalancheChainId, @@ -47,6 +48,7 @@ import { import type { Asset } from '@shapeshiftoss/types' import { abstract, + aptos, arbitrum, avax, base, @@ -443,6 +445,14 @@ export async function getAssets(chainId: ChainId): Promise { explorerAddressLink: ton.explorerAddressLink, explorerTxLink: ton.explorerTxLink, } + case aptosChainId: + return { + assetNamespace: ASSET_NAMESPACE.aptosCoin, + category: adapters.chainIdToCoingeckoAssetPlatform(chainId), + explorer: aptos.explorer, + explorerAddressLink: aptos.explorerAddressLink, + explorerTxLink: aptos.explorerTxLink, + } default: throw new Error(`no coingecko token support for chainId: ${chainId}`) } diff --git a/scripts/generateAssetData/generateAssetData.ts b/scripts/generateAssetData/generateAssetData.ts index 6554fbd430e..28feefe58ae 100644 --- a/scripts/generateAssetData/generateAssetData.ts +++ b/scripts/generateAssetData/generateAssetData.ts @@ -24,6 +24,7 @@ import orderBy from 'lodash/orderBy' import path from 'path' import * as abstract from './abstract' +import * as aptos from './aptos' import * as arbitrum from './arbitrum' import * as avalanche from './avalanche' import * as base from './base' @@ -123,6 +124,7 @@ const generateAssetData = async () => { const suiAssets = await sui.getAssets() const tonAssets = await tonModule.getAssets() const nearAssets = await near.getAssets() + const aptosAssets = await aptos.getAssets() // all assets, included assets to be blacklisted const unfilteredAssetData: Asset[] = [ @@ -178,6 +180,7 @@ const generateAssetData = async () => { ...suiAssets, ...tonAssets, ...nearAssets, + ...aptosAssets, ] // remove blacklisted assets diff --git a/scripts/generateAssetData/generateRelatedAssetIndex/generateChainRelatedAssetIndex.ts b/scripts/generateAssetData/generateRelatedAssetIndex/generateChainRelatedAssetIndex.ts index 2f0592b6878..c52b7c14fb2 100644 --- a/scripts/generateAssetData/generateRelatedAssetIndex/generateChainRelatedAssetIndex.ts +++ b/scripts/generateAssetData/generateRelatedAssetIndex/generateChainRelatedAssetIndex.ts @@ -89,11 +89,13 @@ const manualRelatedAssetIndex: Record = { 'eip155:59144/erc20:0x176211869ca2b568f2a7d4ee941e073a821ee1ff', 'eip155:5000/erc20:0x09bc4e0d864854c6afb6eb9a9cdf58ac190d0df9', 'eip155:146/erc20:0x29219dd400f2bf60e5a23d13be72b486d4038894', + 'aptos:861fb8e6/coin:0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b', ], 'eip155:1/erc20:0xdac17f958d2ee523a2206206994597c13d831ec7': [ 'eip155:59144/erc20:0xa219439258ca9da29e9cc4ce5596924745e12b93', 'eip155:5000/erc20:0x201eba5cc46d216ce6dc03f6a759e8e766e956ae', 'eip155:146/erc20:0x6047828dc181963ba44974801ff68e538da5eaf9', + 'aptos:861fb8e6/coin:0x357b0b74bc833e95a115ad22604854d6b0fca151cecd94111770e5d6ffc9dc2b', ], 'eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f': [ 'eip155:59144/erc20:0x4af15ec2a0bd43db75dd04e62faa3b8ef36b00d5', diff --git a/scripts/generateAssetData/generateRelatedAssetIndex/generateRelatedAssetIndex.ts b/scripts/generateAssetData/generateRelatedAssetIndex/generateRelatedAssetIndex.ts index 8c6b98e686b..6b39a05a2e6 100644 --- a/scripts/generateAssetData/generateRelatedAssetIndex/generateRelatedAssetIndex.ts +++ b/scripts/generateAssetData/generateRelatedAssetIndex/generateRelatedAssetIndex.ts @@ -89,9 +89,11 @@ const manualRelatedAssetIndex: Record = { [cronosAssetId]: ['eip155:1/erc20:0xa0b73e1ff0b80914ab6fe0444e65848c4c34450b'], 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': [ 'eip155:146/erc20:0x29219dd400f2bf60e5a23d13be72b486d4038894', + 'aptos:861fb8e6/coin:0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b', ], 'eip155:1/erc20:0xdac17f958d2ee523a2206206994597c13d831ec7': [ 'eip155:146/erc20:0x6047828dc181963ba44974801ff68e538da5eaf9', + 'aptos:861fb8e6/coin:0x357b0b74bc833e95a115ad22604854d6b0fca151cecd94111770e5d6ffc9dc2b', ], } diff --git a/scripts/generateAssetData/generateTrustWalletUrl/generateTrustWalletUrl.ts b/scripts/generateAssetData/generateTrustWalletUrl/generateTrustWalletUrl.ts index d4986e0d8c4..1fb19e0bfb8 100644 --- a/scripts/generateAssetData/generateTrustWalletUrl/generateTrustWalletUrl.ts +++ b/scripts/generateAssetData/generateTrustWalletUrl/generateTrustWalletUrl.ts @@ -15,6 +15,7 @@ export const generateTrustWalletUrl = (assetId: AssetId) => { sui: 'sui', near: 'near', ton: 'ton', + aptos: 'aptos', } const trustWalletChainName = chainNamespaceToTrustWallet[chainNamespace] diff --git a/src/components/Modals/Send/utils.ts b/src/components/Modals/Send/utils.ts index d1c664bfffa..3bc76dfed87 100644 --- a/src/components/Modals/Send/utils.ts +++ b/src/components/Modals/Send/utils.ts @@ -32,6 +32,7 @@ import { } from '@/hooks/useIsSnapInstalled/useIsSnapInstalled' import { bn, bnOrZero } from '@/lib/bignumber/bignumber' import { assertGetChainAdapter } from '@/lib/utils' +import { assertGetAptosChainAdapter } from '@/lib/utils/aptos' import { assertGetCosmosSdkChainAdapter } from '@/lib/utils/cosmosSdk' import { assertGetEvmChainAdapter, getSupportedEvmChainIds } from '@/lib/utils/evm' import { assertGetNearChainAdapter } from '@/lib/utils/near' @@ -219,6 +220,18 @@ export const estimateFees = async ({ } return adapter.getFeeData(getFeeDataInput) } + case CHAIN_NAMESPACE.Aptos: { + const adapter = assertGetAptosChainAdapter(asset.chainId) + const getFeeDataInput: GetFeeDataInput = { + to, + value, + chainSpecific: { + from: account, + }, + sendMax, + } + return adapter.getFeeData(getFeeDataInput) + } default: throw new Error(`${chainNamespace} not supported`) } @@ -541,6 +554,20 @@ export const handleSendWithMetadata = async ({ } as BuildSendTxInput) } + if (fromChainId(asset.chainId).chainNamespace === CHAIN_NAMESPACE.Aptos) { + const { accountNumber } = bip44Params + const adapter = assertGetAptosChainAdapter(chainId) + + return adapter.buildSendTransaction({ + to, + value, + wallet, + accountNumber, + sendMax: sendInput.sendMax, + chainSpecific: { memo }, + } as BuildSendTxInput) + } + throw new Error(`${chainId} not supported`) })() diff --git a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeExecution.tsx b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeExecution.tsx index a3cc6cfcfc8..39df09a7bc8 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeExecution.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeExecution.tsx @@ -2,6 +2,7 @@ import { bchAssetId, CHAIN_NAMESPACE, fromAccountId, fromChainId } from '@shapes import type { near, SignTx, SignTypedDataInput, ton } from '@shapeshiftoss/chain-adapters' import { ChainAdapterError, toAddressNList } from '@shapeshiftoss/chain-adapters' import type { + AptosSignTx, ETHSignTypedData, SolanaSignTx, StarknetSignTx, @@ -38,6 +39,7 @@ import { HypeLabEvent, trackHypeLabEvent } from '@/lib/hypelab/hypelabSingleton' import { MixPanelEvent } from '@/lib/mixpanel/types' import { TradeExecution } from '@/lib/tradeExecution' import { assertUnreachable } from '@/lib/utils' +import { assertGetAptosChainAdapter } from '@/lib/utils/aptos' import { assertGetCosmosSdkChainAdapter } from '@/lib/utils/cosmosSdk' import { assertGetEvmChainAdapter, signAndBroadcast } from '@/lib/utils/evm' import { assertGetNearChainAdapter } from '@/lib/utils/near' @@ -789,6 +791,39 @@ export const useTradeExecution = ( cancelPollingRef.current = output?.cancelPolling return } + case CHAIN_NAMESPACE.Aptos: { + const adapter = assertGetAptosChainAdapter(stepSellAssetChainId) + + const from = await adapter.getAddress({ + accountNumber, + wallet, + pubKey: + wallet && isTrezor(wallet) ? fromAccountId(sellAssetAccountId).account : undefined, + }) + + const output = await execution.execAptosTransaction({ + swapperName, + tradeQuote, + stepIndex: hopIndex, + slippageTolerancePercentageDecimal, + from, + signAndBroadcastTransaction: async (txToSign: AptosSignTx) => { + const signTxInput = { txToSign, wallet } + const hex = await adapter.signTransaction(signTxInput) + + const output = await adapter.broadcastTransaction({ + senderAddress: from, + receiverAddress, + hex, + }) + + trackMixpanelEventOnExecute() + return output + }, + }) + cancelPollingRef.current = output?.cancelPolling + return + } default: assertUnreachable(stepSellAssetChainNamespace) } diff --git a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeNetworkFeeCryptoBaseUnit.tsx b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeNetworkFeeCryptoBaseUnit.tsx index 7bac2752074..db8d89aa0dc 100644 --- a/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeNetworkFeeCryptoBaseUnit.tsx +++ b/src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeNetworkFeeCryptoBaseUnit.tsx @@ -15,6 +15,7 @@ import { useMemo } from 'react' import { getConfig } from '@/config' import { useWallet } from '@/hooks/useWallet/useWallet' import { assertUnreachable } from '@/lib/utils' +import { assertGetAptosChainAdapter } from '@/lib/utils/aptos' import { assertGetCosmosSdkChainAdapter } from '@/lib/utils/cosmosSdk' import { assertGetEvmChainAdapter } from '@/lib/utils/evm' import { assertGetNearChainAdapter } from '@/lib/utils/near' @@ -315,6 +316,27 @@ export const useTradeNetworkFeeCryptoBaseUnit = ({ }) return output } + case CHAIN_NAMESPACE.Aptos: { + if (!swapper.getAptosTransactionFees) throw Error('missing getAptosTransactionFees') + + const adapter = assertGetAptosChainAdapter(stepSellAssetChainId) + const from = await adapter.getAddress({ + accountNumber, + wallet, + ...(skipDeviceDerivation ? { pubKey } : {}), + }) + + const output = await swapper.getAptosTransactionFees({ + tradeQuote, + from, + stepIndex: hopIndex, + slippageTolerancePercentageDecimal, + chainId: hop.sellAsset.chainId, + config: getConfig(), + assertGetAptosChainAdapter, + }) + return output + } default: assertUnreachable(stepSellAssetChainNamespace) } diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteOrRateInput.ts b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteOrRateInput.ts index 9795b816ce1..a8117d98b78 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteOrRateInput.ts +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteOrRateInput.ts @@ -293,6 +293,25 @@ export const getTradeQuoteOrRateInput = async ({ sendAddress, } as GetTradeQuoteInput } + case CHAIN_NAMESPACE.Aptos: { + const { assertGetAptosChainAdapter } = await import('@/lib/utils/aptos') + const sellAssetChainAdapter = assertGetAptosChainAdapter(sellAsset.chainId) + + const sendAddress = + wallet && sellAccountNumber !== undefined + ? await sellAssetChainAdapter.getAddress({ + accountNumber: sellAccountNumber, + wallet, + pubKey, + }) + : undefined + + return { + ...tradeQuoteInputCommonArgs, + chainId: sellAsset.chainId, + sendAddress, + } as GetTradeQuoteInput + } default: assertUnreachable(chainNamespace) } diff --git a/src/components/TradeAssetSearch/hooks/useGetPopularAssetsQuery.tsx b/src/components/TradeAssetSearch/hooks/useGetPopularAssetsQuery.tsx index 5dce4648192..5afe9981530 100644 --- a/src/components/TradeAssetSearch/hooks/useGetPopularAssetsQuery.tsx +++ b/src/components/TradeAssetSearch/hooks/useGetPopularAssetsQuery.tsx @@ -1,6 +1,7 @@ import type { ChainId } from '@shapeshiftoss/caip' import { abstractAssetId, + aptosAssetId, berachainAssetId, blastAssetId, bobAssetId, @@ -91,6 +92,7 @@ export const queryFn = async () => { if (enabledFlags.Tron) assetIds.push(tronAssetId) if (enabledFlags.Berachain) assetIds.push(berachainAssetId) if (enabledFlags.Sui) assetIds.push(suiAssetId) + if (enabledFlags.Aptos) assetIds.push(aptosAssetId) for (const assetId of [...new Set(assetIds)]) { const asset = primaryAssets[assetId] diff --git a/src/config.ts b/src/config.ts index 7be93398b97..dcb564afd30 100644 --- a/src/config.ts +++ b/src/config.ts @@ -308,6 +308,7 @@ const validators = { VITE_FEATURE_MM_NATIVE_MULTICHAIN: bool({ default: false }), VITE_FEATURE_APTOS: bool({ default: false }), VITE_APTOS_NODE_URL: url(), + VITE_APTOS_INDEXER_URL: url({ default: 'https://api.mainnet.aptoslabs.com/v1/graphql' }), VITE_AGENTIC_SERVER_BASE_URL: url({ default: 'https://api.agent.shapeshift.com', }), diff --git a/src/constants/chains.ts b/src/constants/chains.ts index 53df38a0159..bbb59d82f5f 100644 --- a/src/constants/chains.ts +++ b/src/constants/chains.ts @@ -38,6 +38,7 @@ export const SECOND_CLASS_CHAINS: readonly KnownChainIds[] = [ KnownChainIds.FlowEvmMainnet, KnownChainIds.CeloMainnet, KnownChainIds.AbstractMainnet, + KnownChainIds.AptosMainnet, ] // returns known ChainIds as an array, excluding the ones that are currently flagged off @@ -82,6 +83,7 @@ export const knownChainIds = Object.values(KnownChainIds).filter(chainId => { if (chainId === KnownChainIds.CeloMainnet && !enabledFlags.Celo) return false if (chainId === KnownChainIds.TonMainnet && !enabledFlags.Ton) return false if (chainId === KnownChainIds.ZcashMainnet && !enabledFlags.Zcash) return false + if (chainId === KnownChainIds.AptosMainnet && !enabledFlags.Aptos) return false return true }) diff --git a/src/context/PluginProvider/PluginProvider.tsx b/src/context/PluginProvider/PluginProvider.tsx index 4828d09a47b..a0d8df827f7 100644 --- a/src/context/PluginProvider/PluginProvider.tsx +++ b/src/context/PluginProvider/PluginProvider.tsx @@ -149,6 +149,7 @@ export const PluginProvider = ({ children }: PluginProviderProps): JSX.Element = if (!featureFlags.Ton && chainId === KnownChainIds.TonMainnet) return false if (!featureFlags.Near && chainId === KnownChainIds.NearMainnet) return false if (!featureFlags.Zcash && chainId === KnownChainIds.ZcashMainnet) return false + if (!featureFlags.Aptos && chainId === KnownChainIds.AptosMainnet) return false return true }) diff --git a/src/context/WalletProvider/Ledger/constants.ts b/src/context/WalletProvider/Ledger/constants.ts index ab8b08ceaa1..b7286d8fb06 100644 --- a/src/context/WalletProvider/Ledger/constants.ts +++ b/src/context/WalletProvider/Ledger/constants.ts @@ -1,4 +1,5 @@ import { + aptosAssetId, bchAssetId, btcAssetId, cosmosAssetId, @@ -41,6 +42,7 @@ export const availableLedgerAppAssetIds = [ mayachainAssetId, ...(getConfig().VITE_FEATURE_TRON ? [tronAssetId] : []), ...(getConfig().VITE_FEATURE_NEAR ? [nearAssetId] : []), + ...(getConfig().VITE_FEATURE_APTOS ? [aptosAssetId] : []), ] export const availableLedgerAppChainIds = availableLedgerAppAssetIds.map( diff --git a/src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts b/src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts index 26cc90a0a17..44efd4ab7df 100644 --- a/src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts +++ b/src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts @@ -1,6 +1,7 @@ import type { AccountId, ChainId } from '@shapeshiftoss/caip' import { abstractChainId, + aptosChainId, arbitrumChainId, avalancheChainId, baseChainId, @@ -43,7 +44,6 @@ import { storyChainId, suiChainId, thorchainChainId, - aptosChainId, tonChainId, tronChainId, unichainChainId, @@ -59,6 +59,7 @@ import { isPhantom, isVultisig, supportsAbstract, + supportsAptos, supportsArbitrum, supportsAvalanche, supportsBase, @@ -95,7 +96,6 @@ import { supportsSonic, supportsStarknet, supportsStory, - supportsAptos, supportsSui, supportsThorchain, supportsTron, @@ -226,6 +226,7 @@ export const walletSupportsChain = ({ const isStarknetEnabled = selectFeatureFlag(store.getState(), 'Starknet') const isWorldChainEnabled = selectFeatureFlag(store.getState(), 'WorldChain') const isTonEnabled = selectFeatureFlag(store.getState(), 'Ton') + const isAptosEnabled = selectFeatureFlag(store.getState(), 'Aptos') switch (chainId) { case btcChainId: @@ -324,7 +325,7 @@ export const walletSupportsChain = ({ case suiChainId: return supportsSui(wallet) case aptosChainId: - return supportsAptos(wallet) + return isAptosEnabled && supportsAptos(wallet) case nearChainId: return isNearEnabled && supportsNear(wallet) case starknetChainId: diff --git a/src/lib/account/account.ts b/src/lib/account/account.ts index 7e695a14b68..8ac4bee698c 100644 --- a/src/lib/account/account.ts +++ b/src/lib/account/account.ts @@ -1,6 +1,5 @@ import type { ChainId } from '@shapeshiftoss/caip' import { CHAIN_NAMESPACE, fromChainId } from '@shapeshiftoss/caip' -import type { ChainNamespace } from '@shapeshiftoss/caip' import type { HDWallet } from '@shapeshiftoss/hdwallet-core' import type { AccountMetadataById } from '@shapeshiftoss/types' import merge from 'lodash/merge' diff --git a/src/lib/asset-service/service/AssetService.ts b/src/lib/asset-service/service/AssetService.ts index d1d79e0c90e..9aee3e0c688 100644 --- a/src/lib/asset-service/service/AssetService.ts +++ b/src/lib/asset-service/service/AssetService.ts @@ -2,6 +2,7 @@ import type { AssetId } from '@shapeshiftoss/caip' import { abstractChainId, adapters, + aptosChainId, arbitrumChainId, baseChainId, berachainChainId, @@ -172,6 +173,7 @@ class _AssetService { if (!config.VITE_FEATURE_ZCASH && asset.chainId === zecChainId) return false if (!config.VITE_FEATURE_STARKNET && asset.chainId === starknetChainId) return false if (!config.VITE_FEATURE_TON && asset.chainId === tonChainId) return false + if (!config.VITE_FEATURE_APTOS && asset.chainId === aptosChainId) return false return true }) diff --git a/src/lib/coingecko/constants.ts b/src/lib/coingecko/constants.ts index 8da9a8fb075..2c57b174836 100644 --- a/src/lib/coingecko/constants.ts +++ b/src/lib/coingecko/constants.ts @@ -1,6 +1,7 @@ import type { AssetId } from '@shapeshiftoss/caip' import { adapters, + aptosAssetId, baseAssetId, bchAssetId, bscAssetId, @@ -40,4 +41,5 @@ export const COINGECKO_NATIVE_ASSET_ID_TO_ASSET_ID: Partial { ...(getConfig().VITE_FEATURE_TON ? [tonChainId] : []), ...(getConfig().VITE_FEATURE_FLOWEVM ? [flowEvmChainId] : []), ...(getConfig().VITE_FEATURE_CELO ? [celoChainId] : []), + ...(getConfig().VITE_FEATURE_APTOS ? [aptosChainId] : []), ] } diff --git a/src/lib/tradeExecution.ts b/src/lib/tradeExecution.ts index a891bec7ada..58c5ae99952 100644 --- a/src/lib/tradeExecution.ts +++ b/src/lib/tradeExecution.ts @@ -1,5 +1,6 @@ import { fromAccountId } from '@shapeshiftoss/caip' import type { + AptosTransactionExecutionInput, CommonGetUnsignedTransactionArgs, CommonTradeExecutionInput, CosmosSdkTransactionExecutionInput, @@ -35,6 +36,7 @@ import { TxStatus } from '@shapeshiftoss/unchained-client' import axios from 'axios' import { EventEmitter } from 'node:events' +import { assertGetAptosChainAdapter } from './utils/aptos' import { assertGetCosmosSdkChainAdapter } from './utils/cosmosSdk' import { assertGetEvmChainAdapter } from './utils/evm' import { assertGetNearChainAdapter } from './utils/near' @@ -107,6 +109,7 @@ export const fetchTradeStatus = async ({ assertGetSuiChainAdapter, assertGetNearChainAdapter, assertGetStarknetChainAdapter, + assertGetAptosChainAdapter, fetchIsSmartContractAddressQuery, }) @@ -918,4 +921,55 @@ export class TradeExecution { buildSignBroadcast, ) } + + async execAptosTransaction({ + swapperName, + tradeQuote, + stepIndex, + slippageTolerancePercentageDecimal, + from, + signAndBroadcastTransaction, + }: AptosTransactionExecutionInput) { + const buildSignBroadcast = async ( + swapper: Swapper & SwapperApi, + { + tradeQuote, + chainId, + stepIndex, + slippageTolerancePercentageDecimal, + config, + }: CommonGetUnsignedTransactionArgs, + ) => { + if (!swapper.getUnsignedAptosTransaction) { + throw Error('missing implementation for getUnsignedAptosTransaction') + } + if (!swapper.executeAptosTransaction) { + throw Error('missing implementation for executeAptosTransaction') + } + + const unsignedTxResult = await swapper.getUnsignedAptosTransaction({ + tradeQuote, + chainId, + stepIndex, + slippageTolerancePercentageDecimal, + from, + config, + assertGetAptosChainAdapter, + }) + + return await swapper.executeAptosTransaction(unsignedTxResult, { + signAndBroadcastTransaction, + }) + } + + return await this._execWalletAgnostic( + { + swapperName, + tradeQuote, + stepIndex, + slippageTolerancePercentageDecimal, + }, + buildSignBroadcast, + ) + } } diff --git a/src/lib/utils/aptos.ts b/src/lib/utils/aptos.ts index 1bd797bf817..66329984f7e 100644 --- a/src/lib/utils/aptos.ts +++ b/src/lib/utils/aptos.ts @@ -27,7 +27,7 @@ export const assertGetAptosChainAdapter = ( export const getAptosTransactionStatus = async (txHash: string): Promise => { try { const adapter = assertGetAptosChainAdapter(aptosChainId) - const rpcUrl = (adapter as unknown as { rpcUrl: string }).rpcUrl + const rpcUrl = adapter.getRpcUrl() const response = await fetch(`${rpcUrl}/transactions/by_hash/${txHash}`) if (!response.ok) return TxStatus.Unknown diff --git a/src/pages/Markets/components/MarketsRow.tsx b/src/pages/Markets/components/MarketsRow.tsx index 03af8ca0daf..6885250347b 100644 --- a/src/pages/Markets/components/MarketsRow.tsx +++ b/src/pages/Markets/components/MarketsRow.tsx @@ -106,6 +106,7 @@ export const MarketsRow: React.FC = ({ const isFlowEvmEnabled = useAppSelector(state => selectFeatureFlag(state, 'FlowEvm')) const isCeloEnabled = useAppSelector(state => selectFeatureFlag(state, 'Celo')) const isSeiEnabled = useAppSelector(state => selectFeatureFlag(state, 'Sei')) + const isAptosEnabled = useAppSelector(state => selectFeatureFlag(state, 'Aptos')) const [isSmallerThanLg] = useMediaQuery(`(max-width: ${breakpoints.lg})`) const chainIds = useMemo(() => { @@ -139,6 +140,7 @@ export const MarketsRow: React.FC = ({ if (!isEtherealEnabled && chainId === KnownChainIds.EtherealMainnet) return false if (!isFlowEvmEnabled && chainId === KnownChainIds.FlowEvmMainnet) return false if (!isCeloEnabled && chainId === KnownChainIds.CeloMainnet) return false + if (!isAptosEnabled && chainId === KnownChainIds.AptosMainnet) return false return true }) }, [ @@ -170,6 +172,7 @@ export const MarketsRow: React.FC = ({ isFlowEvmEnabled, isCeloEnabled, isSeiEnabled, + isAptosEnabled, ]) const Title = useMemo(() => { diff --git a/src/pages/RFOX/components/Stake/Bridge/hooks/useRfoxBridge.ts b/src/pages/RFOX/components/Stake/Bridge/hooks/useRfoxBridge.ts index da64c69e474..8bf98bf5df4 100644 --- a/src/pages/RFOX/components/Stake/Bridge/hooks/useRfoxBridge.ts +++ b/src/pages/RFOX/components/Stake/Bridge/hooks/useRfoxBridge.ts @@ -25,6 +25,7 @@ import { useWallet } from '@/hooks/useWallet/useWallet' import { getMixPanel } from '@/lib/mixpanel/mixPanelSingleton' import { fetchTradeStatus } from '@/lib/tradeExecution' import { assertGetChainAdapter } from '@/lib/utils' +import { assertGetAptosChainAdapter } from '@/lib/utils/aptos' import { assertGetCosmosSdkChainAdapter } from '@/lib/utils/cosmosSdk' import { assertGetEvmChainAdapter, signAndBroadcast } from '@/lib/utils/evm' import { assertGetNearChainAdapter } from '@/lib/utils/near' @@ -188,6 +189,7 @@ export const useRfoxBridge = ({ confirmedQuote }: UseRfoxBridgeProps): UseRfoxBr assertGetSuiChainAdapter, assertGetNearChainAdapter, assertGetStarknetChainAdapter, + assertGetAptosChainAdapter, getEthersV5Provider, fetchIsSmartContractAddressQuery, viemClientByChainId, diff --git a/src/plugins/activePlugins.ts b/src/plugins/activePlugins.ts index 1e3aef28105..83dc5241efd 100644 --- a/src/plugins/activePlugins.ts +++ b/src/plugins/activePlugins.ts @@ -1,4 +1,5 @@ import abstract from '@/plugins/abstract' +import aptos from '@/plugins/aptos' import arbitrum from '@/plugins/arbitrum' import avalanche from '@/plugins/avalanche' import base from '@/plugins/base' @@ -102,4 +103,5 @@ export const activePlugins = [ zcash, zksyncera, abstract, + aptos, ] diff --git a/src/plugins/aptos/index.tsx b/src/plugins/aptos/index.tsx index 0cdcd3ea4ef..2d6658dbaae 100644 --- a/src/plugins/aptos/index.tsx +++ b/src/plugins/aptos/index.tsx @@ -19,6 +19,7 @@ export default function register(): Plugins { () => { return new aptos.ChainAdapter({ rpcUrl: getConfig().VITE_APTOS_NODE_URL, + indexerUrl: getConfig().VITE_APTOS_INDEXER_URL, }) }, ], diff --git a/src/state/apis/swapper/helpers/swapperApiHelpers.ts b/src/state/apis/swapper/helpers/swapperApiHelpers.ts index 32b8f86e246..a204defed0d 100644 --- a/src/state/apis/swapper/helpers/swapperApiHelpers.ts +++ b/src/state/apis/swapper/helpers/swapperApiHelpers.ts @@ -16,6 +16,7 @@ import { queryClient } from '@/context/QueryClientProvider/queryClient' import { fetchIsSmartContractAddressQuery } from '@/hooks/useIsSmartContractAddress/useIsSmartContractAddress' import { getMixPanel } from '@/lib/mixpanel/mixPanelSingleton' import { assertGetChainAdapter } from '@/lib/utils' +import { assertGetAptosChainAdapter } from '@/lib/utils/aptos' import { assertGetCosmosSdkChainAdapter } from '@/lib/utils/cosmosSdk' import { assertGetEvmChainAdapter } from '@/lib/utils/evm' import { assertGetNearChainAdapter } from '@/lib/utils/near' @@ -58,6 +59,7 @@ export const createSwapperDeps = (state: ReduxState): SwapperDeps => ({ assertGetSuiChainAdapter, assertGetNearChainAdapter, assertGetStarknetChainAdapter, + assertGetAptosChainAdapter, fetchIsSmartContractAddressQuery, config: getConfig(), mixPanel: getMixPanel(), diff --git a/src/state/slices/opportunitiesSlice/mappings.ts b/src/state/slices/opportunitiesSlice/mappings.ts index 198cd76b6d1..0c104e97b9f 100644 --- a/src/state/slices/opportunitiesSlice/mappings.ts +++ b/src/state/slices/opportunitiesSlice/mappings.ts @@ -199,6 +199,7 @@ export const CHAIN_ID_TO_SUPPORTED_DEFI_OPPORTUNITIES: Record< [KnownChainIds.BlastMainnet]: [], [KnownChainIds.AbstractMainnet]: [], [KnownChainIds.HemiMainnet]: [], + [KnownChainIds.AptosMainnet]: [], } // Single opportunity metadata resolvers diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index d50cd88effd..d7a35caaae9 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -224,6 +224,7 @@ interface ImportMetaEnv { readonly VITE_FEATURE_NOTIFICATIONS_WEBSERVICES: string readonly VITE_FEATURE_APTOS: string readonly VITE_APTOS_NODE_URL: string + readonly VITE_APTOS_INDEXER_URL: string // Only present in *some* envs readonly VITE_MIXPANEL_TOKEN?: string From 3fe9d95aa62b81e73a957f9777ad213b0e3a8347 Mon Sep 17 00:00:00 2001 From: Discostu Date: Sat, 16 May 2026 23:48:48 +0200 Subject: [PATCH 09/16] fix(aptos): use gas_used not max_gas_amount for fee estimation The Aptos node, when called with estimate_max_gas_amount=true, overwrites max_gas_amount in the response with the sender's AFFORDABILITY ceiling (balance / gas_unit_price), NOT a tight recommendation. Reading that field as the gas estimate caused fees to be inflated by 100-300x. For a wallet holding 1.08 APT at gas_unit_price=100, the node returned max_gas_amount=1,084,352. Multiplied by the prioritized gas estimate (150 octas), the displayed network fee came out to 1.62 APT (~\$1.54) for what should be a ~0.01 APT transfer. Fix: prefer sim.gas_used (real simulated consumption) with the Aptos CLI's 1.5x safety factor (gas_used * 3 / 2). Cap at the affordability ceiling defensively. Bump MIN_MAX_GAS_AMOUNT from 12,000 to 20,000 to safely cover real transfer_coins consumption (historical on-chain: 5,500-10,500 units) when simulation fails (e.g., dummy pubkey rejected by INVALID_AUTH_KEY). Refs: - https://aptos.dev/build/guides/system-integrators-guide ("estimate uses min(max_gas_amount, gas_used * safety_factor)", CLI safety_factor = 1.5) - https://aptos.dev/network/blockchain/gas-txn-fee - Direct fullnode simulation confirmed max_gas_amount echoed as account_balance / gas_unit_price when the flag is set. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../chain-adapters/src/aptos/AptosChainAdapter.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/chain-adapters/src/aptos/AptosChainAdapter.ts b/packages/chain-adapters/src/aptos/AptosChainAdapter.ts index 9b91f0a6ec6..b48dd713700 100644 --- a/packages/chain-adapters/src/aptos/AptosChainAdapter.ts +++ b/packages/chain-adapters/src/aptos/AptosChainAdapter.ts @@ -50,7 +50,7 @@ export interface ChainAdapterArgs { } const APT_COIN_TYPE = '0x1::aptos_coin::AptosCoin' -const MIN_MAX_GAS_AMOUNT = 12_000n +const MIN_MAX_GAS_AMOUNT = 20_000n export class ChainAdapter implements IChainAdapter { static readonly rootBip44Params: RootBip44Params = { @@ -234,7 +234,14 @@ export class ChainAdapter implements IChainAdapter { transaction: tx, options: { estimateGasUnitPrice: true, estimateMaxGasAmount: true }, }) - const recommended = BigInt(sim?.max_gas_amount ?? sim?.gas_used ?? 0) + // sim.max_gas_amount with estimateMaxGasAmount=true is the AFFORDABILITY ceiling + // (sender balance / gas_unit_price), NOT a recommendation. The real consumption is + // sim.gas_used. Apply the Aptos CLI 1.5x safety factor, capped by affordability. + const gasUsed = BigInt(sim?.gas_used ?? 0) + if (gasUsed === 0n) return MIN_MAX_GAS_AMOUNT + const ceiling = BigInt(sim?.max_gas_amount ?? 0) + const withBuffer = (gasUsed * 3n) / 2n + const recommended = ceiling > 0n && ceiling < withBuffer ? ceiling : withBuffer return recommended > MIN_MAX_GAS_AMOUNT ? recommended : MIN_MAX_GAS_AMOUNT } From 9044d03f7d7b378815d18c37ec8288fafa92a5a6 Mon Sep 17 00:00:00 2001 From: Discostu Date: Sun, 17 May 2026 02:33:45 +0200 Subject: [PATCH 10/16] fix(aptos): address CodeRabbit review findings (9 items) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves all 9 actionable comments from the CodeRabbit review on the Aptos chain integration PR. Chain adapter (packages/chain-adapters/src/aptos/AptosChainAdapter.ts): - validateAddress now uses AccountAddress.fromString() from the SDK, which correctly accepts both short-form (0x1) and full-form Aptos addresses, instead of a rigid regex that rejected 0x1. - parseTx status logic now handles all three cases explicitly: success === true → Confirmed, false → Failed, undefined (pending tx) → Pending. Previously undefined was incorrectly treated as Confirmed. - Sender/recipient equality now goes through AccountAddress.equals via a normalized eq() helper so casing/length variants of the same Aptos address compare correctly. - buildSendApiTransaction now honors chainSpecific.coinType so non-APT CoinStore coins on Aptos can be transferred via NEAR Intents (or any future swapper). Aptos types (packages/chain-adapters/src/aptos/types.ts): - BuildTxInput gains coinType?: string (Aptos CoinStore type, defaults to APT). NEAR Intents Aptos route (packages/swapper/src/swappers/NearIntentsSwapper/endpoints.ts): - getUnsignedAptosTransaction detects non-native Aptos coins via assetNamespace (slip44 = native APT, otherwise assetReference is the Move CoinStore type) and forwards coinType to the chain adapter. Aptos swap status check (packages/swapper/src/utils.ts): - checkAptosSwapStatus normalizes recipient addresses via AccountAddress before comparing, replacing the substring-style t.to.includes(address) check. Aptos util (src/lib/utils/aptos.ts): - isAptosChainAdapter type guard now checks that getChainId is a function and wraps the call in try/catch so plain objects can't throw. Native HDWallet (packages/hdwallet-native/src/aptos.ts): - aptosNextAccountPath now delegates to core.aptosNextAccountPath instead of throwing. Treasury (packages/utils/src/treasury.ts): - Drop KnownChainIds.AptosMainnet from treasuryChainIds and remove the placeholder DAO_TREASURY_APTOS constant. The Aptos treasury entry will be added when the real DAO multisig address is available. Treasury helpers (packages/swapper/src/swappers/utils/helpers/helpers.ts): - Drop the AptosMainnet entry from DAO_TREASURY_BY_CHAIN_ID. Ledger constants (src/context/WalletProvider/Ledger/constants.ts): - Remove conditional inclusion of aptosAssetId from availableLedgerAppAssetIds. LedgerHDWallet does not implement AptosWallet; Aptos Ledger support will land in a focused follow-up PR with physical-device testing. Tests (packages/chain-adapters/src/aptos/AptosChainAdapter.test.ts): - vi.mock now keeps the real exports of @aptos-labs/ts-sdk (AccountAddress, Ed25519PublicKey, etc.) and only mocks the network-using Aptos client, AptosConfig, and Network. Without this, the new AccountAddress.equals comparisons would throw silently. - validateAddress test cases updated to reflect the SDK's behavior: short-form (0x1) and unprefixed full-form are now accepted; only truly malformed inputs are rejected. Refs: CodeRabbit review on PR #12349 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/aptos/AptosChainAdapter.test.ts | 22 +++++++--- .../src/aptos/AptosChainAdapter.ts | 43 +++++++++++-------- packages/chain-adapters/src/aptos/types.ts | 3 ++ packages/hdwallet-native/src/aptos.ts | 5 +-- .../swappers/NearIntentsSwapper/endpoints.ts | 10 ++++- .../src/swappers/utils/helpers/helpers.ts | 2 - packages/swapper/src/utils.ts | 14 +++++- packages/utils/src/treasury.ts | 5 --- .../WalletProvider/Ledger/constants.ts | 2 - src/lib/utils/aptos.ts | 8 +++- 10 files changed, 75 insertions(+), 39 deletions(-) diff --git a/packages/chain-adapters/src/aptos/AptosChainAdapter.test.ts b/packages/chain-adapters/src/aptos/AptosChainAdapter.test.ts index c8694bc2681..ef2cfd7b798 100644 --- a/packages/chain-adapters/src/aptos/AptosChainAdapter.test.ts +++ b/packages/chain-adapters/src/aptos/AptosChainAdapter.test.ts @@ -1,3 +1,4 @@ +import type * as AptosSdkActual from '@aptos-labs/ts-sdk' import { aptosAssetId, aptosChainId } from '@shapeshiftoss/caip' import { KnownChainIds } from '@shapeshiftoss/types' import { TransferType, TxStatus } from '@shapeshiftoss/unchained-client' @@ -6,8 +7,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { ValidAddressResultType } from '../types' import { ChainAdapter } from './AptosChainAdapter' -vi.mock('@aptos-labs/ts-sdk', () => { +vi.mock('@aptos-labs/ts-sdk', async () => { + // Keep real exports (AccountAddress, Ed25519PublicKey, BCS helpers, etc.) and only + // mock the network-using Aptos client + AptosConfig + Network enum. + const actual = await vi.importActual('@aptos-labs/ts-sdk') return { + ...actual, Aptos: vi.fn().mockImplementation(() => ({ getAccountCoinsData: vi.fn(), getGasPriceEstimation: vi.fn(), @@ -71,20 +76,25 @@ describe('AptosChainAdapter', () => { expect(r).toEqual({ valid: true, result: ValidAddressResultType.Valid }) }) - it('rejects addresses without 0x prefix', async () => { + it('accepts 64-hex address without 0x prefix (Aptos SDK normalizes)', async () => { const r = await adapter.validateAddress(ADDR.slice(2)) - expect(r.valid).toBe(false) + expect(r).toEqual({ valid: true, result: ValidAddressResultType.Valid }) }) - it('rejects addresses of wrong length', async () => { - const r = await adapter.validateAddress('0xabcd') - expect(r.valid).toBe(false) + it('accepts Aptos special short-form addresses (e.g. 0x1)', async () => { + const r = await adapter.validateAddress('0x1') + expect(r).toEqual({ valid: true, result: ValidAddressResultType.Valid }) }) it('rejects non-hex content', async () => { const r = await adapter.validateAddress('0x' + 'z'.repeat(64)) expect(r.valid).toBe(false) }) + + it('rejects garbage strings', async () => { + const r = await adapter.validateAddress('not-an-address') + expect(r.valid).toBe(false) + }) }) describe('getAccount', () => { diff --git a/packages/chain-adapters/src/aptos/AptosChainAdapter.ts b/packages/chain-adapters/src/aptos/AptosChainAdapter.ts index b48dd713700..3498079c30f 100644 --- a/packages/chain-adapters/src/aptos/AptosChainAdapter.ts +++ b/packages/chain-adapters/src/aptos/AptosChainAdapter.ts @@ -1,5 +1,6 @@ import type { InputEntryFunctionData } from '@aptos-labs/ts-sdk' import { + AccountAddress, AccountAuthenticatorEd25519, Aptos, AptosConfig, @@ -196,19 +197,7 @@ export class ChainAdapter implements IChainAdapter { validateAddress(address: string): Promise { try { - if (!address.startsWith('0x')) { - return Promise.resolve({ valid: false, result: ValidAddressResultType.Invalid }) - } - - const hexPart = address.slice(2) - if (hexPart.length !== 64) { - return Promise.resolve({ valid: false, result: ValidAddressResultType.Invalid }) - } - - if (!/^[0-9a-fA-F]{64}$/.test(hexPart)) { - return Promise.resolve({ valid: false, result: ValidAddressResultType.Invalid }) - } - + AccountAddress.fromString(address) return Promise.resolve({ valid: true, result: ValidAddressResultType.Valid }) } catch { return Promise.resolve({ valid: false, result: ValidAddressResultType.Invalid }) @@ -249,11 +238,12 @@ export class ChainAdapter implements IChainAdapter { input: BuildSendApiTxInput, ): Promise> { try { - const { from, accountNumber, to, value } = input + const { from, accountNumber, to, value, chainSpecific } = input + const coinType = chainSpecific?.coinType ?? APT_COIN_TYPE const data: InputEntryFunctionData = { function: '0x1::aptos_account::transfer_coins', - typeArguments: [APT_COIN_TYPE], + typeArguments: [coinType], functionArguments: [to, BigInt(value)], } const maxGasAmount = Number(await this.estimateMaxGasAmount(from, data)) @@ -501,7 +491,14 @@ export class ChainAdapter implements IChainAdapter { const blockHeight = Number(tx.version ?? 0) const blockTime = tx.timestamp ? Math.floor(Number(tx.timestamp) / 1_000_000) : 0 - const status = tx.success === false ? TxStatus.Failed : TxStatus.Confirmed + // Pending Aptos txs have undefined `success` until executed on-chain. + // Only mark Confirmed/Failed when we have a definitive answer. + const status = + tx.success === true + ? TxStatus.Confirmed + : tx.success === false + ? TxStatus.Failed + : TxStatus.Pending const gasUsed = tx.gas_used ?? '0' const gasUnitPrice = tx.gas_unit_price ?? '0' @@ -522,7 +519,17 @@ export class ChainAdapter implements IChainAdapter { const amount = String(args[isFaTransfer ? 2 : 1] ?? '0') const sender = tx.sender ?? '' - if (sender === pubkey) { + // Aptos addresses may differ in casing/length but refer to the same account. + // Use SDK normalization via AccountAddress for reliable equality. + const eq = (a: string, b: string) => { + try { + return AccountAddress.fromString(a).equals(AccountAddress.fromString(b)) + } catch { + return false + } + } + + if (eq(sender, pubkey)) { transfers.push({ assetId: transferAssetId, from: [sender], @@ -531,7 +538,7 @@ export class ChainAdapter implements IChainAdapter { value: amount, }) } - if (recipient === pubkey) { + if (eq(recipient, pubkey)) { transfers.push({ assetId: transferAssetId, from: [sender], diff --git a/packages/chain-adapters/src/aptos/types.ts b/packages/chain-adapters/src/aptos/types.ts index 15f43520c29..d5f708e9322 100644 --- a/packages/chain-adapters/src/aptos/types.ts +++ b/packages/chain-adapters/src/aptos/types.ts @@ -15,6 +15,9 @@ export type AptosAccount = { export type BuildTxInput = { memo?: string + // Aptos CoinStore-style coin type to transfer (e.g. '0x1::aptos_coin::AptosCoin'). + // Defaults to APT when omitted. + coinType?: string } export type AptosGetFeeDataInput = { diff --git a/packages/hdwallet-native/src/aptos.ts b/packages/hdwallet-native/src/aptos.ts index 81261acacf6..d2d874120bf 100644 --- a/packages/hdwallet-native/src/aptos.ts +++ b/packages/hdwallet-native/src/aptos.ts @@ -15,9 +15,8 @@ export function MixinNativeAptosWalletInfo = { [KnownChainIds.MonadMainnet]: DAO_TREASURY_MONAD, [KnownChainIds.HyperEvmMainnet]: DAO_TREASURY_HYPEREVM, [KnownChainIds.BobMainnet]: DAO_TREASURY_BOB, - [KnownChainIds.AptosMainnet]: DAO_TREASURY_APTOS, } export const getTreasuryAddressFromChainId = (chainId: ChainId): string => { diff --git a/packages/swapper/src/utils.ts b/packages/swapper/src/utils.ts index 69875a56440..55dca5a4e58 100644 --- a/packages/swapper/src/utils.ts +++ b/packages/swapper/src/utils.ts @@ -1,3 +1,4 @@ +import { AccountAddress } from '@aptos-labs/ts-sdk' import type { AssetId, ChainId } from '@shapeshiftoss/caip' import { aptosChainId, solanaChainId, starknetChainId, suiChainId } from '@shapeshiftoss/caip' import type { @@ -532,8 +533,19 @@ export const checkAptosSwapStatus = async ({ const adapter = assertGetAptosChainAdapter(aptosChainId) const tx = await adapter.parseTx(txHash, address) + // Aptos addresses can differ in casing/length but refer to the same account. + // Normalize via AccountAddress for reliable equality. + const target = AccountAddress.fromString(address) + const isTargetAddress = (candidate: string) => { + try { + return AccountAddress.fromString(candidate).equals(target) + } catch { + return false + } + } + const receiveTransfer = tx.transfers.find( - t => t.type === TransferType.Receive && t.to.includes(address), + t => t.type === TransferType.Receive && t.to.some(isTargetAddress), ) const actualBuyAmountCryptoBaseUnit = receiveTransfer?.value diff --git a/packages/utils/src/treasury.ts b/packages/utils/src/treasury.ts index 0d32d19b861..66d65bf8db9 100644 --- a/packages/utils/src/treasury.ts +++ b/packages/utils/src/treasury.ts @@ -20,7 +20,6 @@ export const treasuryChainIds = [ KnownChainIds.MonadMainnet, KnownChainIds.HyperEvmMainnet, KnownChainIds.BobMainnet, - KnownChainIds.AptosMainnet, ] as const export type TreasuryChainId = (typeof treasuryChainIds)[number] @@ -48,7 +47,3 @@ export const DAO_TREASURY_BOB = '0xF5AA59151bE6515C4Ca68A0282CF68B3eA4846fC' export const DAO_TREASURY_STARKNET = '0x07ac2252f2da7cbf085e7a5ddc1318243aa818607cdd430dd2e17dd5d487606a' export const DAO_TREASURY_TON = 'UQAHHeOhXst-zSGGigQ8KgDzz89nACBR4TxXwXNjU4DsriLb' -// TODO: replace with the real ShapeShift DAO Aptos multisig address before shipping to prod. -// See https://forum.shapeshift.com/thread/dao-treasuries-and-multisigs-43646 -export const DAO_TREASURY_APTOS = - '0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddead' diff --git a/src/context/WalletProvider/Ledger/constants.ts b/src/context/WalletProvider/Ledger/constants.ts index b7286d8fb06..ab8b08ceaa1 100644 --- a/src/context/WalletProvider/Ledger/constants.ts +++ b/src/context/WalletProvider/Ledger/constants.ts @@ -1,5 +1,4 @@ import { - aptosAssetId, bchAssetId, btcAssetId, cosmosAssetId, @@ -42,7 +41,6 @@ export const availableLedgerAppAssetIds = [ mayachainAssetId, ...(getConfig().VITE_FEATURE_TRON ? [tronAssetId] : []), ...(getConfig().VITE_FEATURE_NEAR ? [nearAssetId] : []), - ...(getConfig().VITE_FEATURE_APTOS ? [aptosAssetId] : []), ] export const availableLedgerAppChainIds = availableLedgerAppAssetIds.map( diff --git a/src/lib/utils/aptos.ts b/src/lib/utils/aptos.ts index 66329984f7e..a7fe3293d06 100644 --- a/src/lib/utils/aptos.ts +++ b/src/lib/utils/aptos.ts @@ -8,7 +8,13 @@ import { getChainAdapterManager } from '@/context/PluginProvider/chainAdapterSin export const isAptosChainAdapter = (chainAdapter: unknown): chainAdapter is aptos.ChainAdapter => { if (!chainAdapter || typeof chainAdapter !== 'object') return false - return (chainAdapter as aptos.ChainAdapter).getChainId() === aptosChainId + const candidate = chainAdapter as { getChainId?: unknown } + if (typeof candidate.getChainId !== 'function') return false + try { + return (chainAdapter as aptos.ChainAdapter).getChainId() === aptosChainId + } catch { + return false + } } export const assertGetAptosChainAdapter = ( From 838966d56a30344e7096779071696793383b6c38 Mon Sep 17 00:00:00 2001 From: Discostu Date: Sun, 17 May 2026 03:23:58 +0200 Subject: [PATCH 11/16] fix(chain-adapters): bail verifyLedgerAppOpen for non-Ledger wallets first verifyLedgerAppOpen() previously called getCoin() and getLedgerAppName() unconditionally before checking the wallet type. Both switches throw "Unsupported chainId" for any chain not enumerated (e.g. Aptos in this PR, since the Aptos Ledger commit was intentionally deferred). Result: AptosChainAdapter.getAddress() threw for Native wallets too because the chain adapter calls verifyLedgerAppOpen() in its address path. Move the isLedger(wallet) early-return to the top of the function so the chain lookups only happen for actual Ledger wallets. Native and other non-Ledger wallets bypass the gate entirely. Side benefit: future chain additions to the app don't need a simultaneous ledgerAppGate.ts update unless they actually ship Ledger support. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/chain-adapters/src/utils/ledgerAppGate.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/chain-adapters/src/utils/ledgerAppGate.ts b/packages/chain-adapters/src/utils/ledgerAppGate.ts index a57d8fb0361..a38935dfeff 100644 --- a/packages/chain-adapters/src/utils/ledgerAppGate.ts +++ b/packages/chain-adapters/src/utils/ledgerAppGate.ts @@ -86,11 +86,14 @@ const getCoin = (chainId: ChainId | KnownChainIds) => { } export const verifyLedgerAppOpen = async (chainId: ChainId | KnownChainIds, wallet: HDWallet) => { + // Bail out for non-Ledger wallets BEFORE any chain lookup so we don't throw + // for chains that aren't enumerated in the getCoin/getLedgerAppName switches + // (e.g. Aptos, which is intentionally unsupported on Ledger in this PR). + if (!isLedger(wallet)) return + const coin = getCoin(chainId) const appName = getLedgerAppName(chainId) - if (!isLedger(wallet)) return - const isAppOpen = async () => { try { await wallet.validateCurrentApp(coin) From c5cd2609fd6d5b89a00e17698a75cb0889ce8a61 Mon Sep 17 00:00:00 2001 From: Discostu Date: Sun, 17 May 2026 05:20:12 +0200 Subject: [PATCH 12/16] refactor(aptos): use CoinGecko tokenlist for asset generation Replaces the bespoke Panora-based fetcher with the same CoinGecko tokenlist pattern used by every other non-EVM chain (Solana, Sui, Near, Ton, Tron). Removes the VITE_PANORA_API_KEY requirement from the asset generation pipeline. CoinGecko exposes two different platform slugs for Aptos: their /api/v3 endpoints (and our CoinGecko adapter) use 'aptos-network', but the static tokenlist at tokens.coingecko.com only responds to 'aptos' ('aptos-network' returns 403 there). Hardcoded the right slug in scripts/generateAssetData/coingecko.ts to keep the existing 'aptos-network' adapter value untouched for API consumers. Verified `pnpm run generate:chain aptos` returns 70 assets from CoinGecko without any API key. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/generateAssetData/aptos/index.ts | 84 +++++------------------- scripts/generateAssetData/coingecko.ts | 6 +- 2 files changed, 20 insertions(+), 70 deletions(-) diff --git a/scripts/generateAssetData/aptos/index.ts b/scripts/generateAssetData/aptos/index.ts index 115a08c33f9..8be615e2687 100644 --- a/scripts/generateAssetData/aptos/index.ts +++ b/scripts/generateAssetData/aptos/index.ts @@ -1,81 +1,27 @@ -import { aptosChainId, ASSET_NAMESPACE, toAssetId } from '@shapeshiftoss/caip' +import { aptosChainId } from '@shapeshiftoss/caip' import type { Asset } from '@shapeshiftoss/types' import { aptos, unfreeze } from '@shapeshiftoss/utils' -import axios from 'axios' import uniqBy from 'lodash/uniqBy' -const PANORA_API_BASE = 'https://api.panora.exchange' +import * as coingecko from '../coingecko' -// Aptos has no public CoinGecko token list (their tokens endpoint returns 403), -// so we use Panora's tokenlist as the source of truth for tradeable assets. -// Panora is the DEX aggregator we integrate with, so this matches what users can swap. - -type PanoraToken = { - chainId: number - tokenAddress: string | null - faAddress: string | null - name: string - symbol: string - decimals: number - logoUrl: string | null - panoraUI: boolean - panoraTags: string[] | null - isBanned: boolean -} - -type PanoraResponse = { status: string; data: PanoraToken[] } | PanoraToken[] - -// Trusted tag set — excludes "Emojicoin" / "Meme"-only spam. -// Per Panora's taxonomy: Native/Verified are official, Bridged is canonical cross-chain -// (e.g. LayerZero USDC), Recognized/RWA cover BUIDL etc. -const TRUSTED_TAGS = new Set(['Verified', 'Recognized', 'RWA', 'Bridged']) - -const fetchPanoraTokens = async (): Promise => { - const apiKey = process.env.VITE_PANORA_API_KEY - if (!apiKey) throw new Error('Missing VITE_PANORA_API_KEY - source ~/.zshrc or set it') +export const getAssets = async (): Promise => { + const results = await Promise.allSettled([coingecko.getAssets(aptosChainId)]) - const { data } = await axios.get(`${PANORA_API_BASE}/tokenlist`, { - headers: { 'x-api-key': apiKey }, - timeout: 30000, + const [assets] = results.map(result => { + if (result.status === 'fulfilled') return result.value + console.error('Error fetching Aptos assets from CoinGecko:', result.reason) + return [] }) - const tokens = Array.isArray(data) ? data : data.data - - return tokens - .filter(t => t.panoraUI && !t.isBanned) - .filter(t => (t.panoraTags ?? []).some(tag => TRUSTED_TAGS.has(tag))) - .filter(t => Boolean(t.faAddress || t.tokenAddress)) - .filter(t => t.tokenAddress !== '0x1::aptos_coin::AptosCoin' && t.faAddress !== '0xa') - .map(token => { - // Prefer faAddress (modern Aptos Fungible Asset standard, universally supported by Panora). - // Falls back to tokenAddress for legacy coin-standard-only tokens. - const assetReference = (token.faAddress ?? token.tokenAddress) as string - const assetId = toAssetId({ - chainId: aptosChainId, - assetNamespace: ASSET_NAMESPACE.aptosCoin, - assetReference, - }) - const asset: Asset = { - assetId, - chainId: aptosChainId, - name: token.name, - symbol: token.symbol, - precision: token.decimals, - color: aptos.color, - icon: token.logoUrl ?? '', - explorer: aptos.explorer, - explorerAddressLink: aptos.explorerAddressLink, - explorerTxLink: aptos.explorerTxLink, - relatedAssetKey: null, - } - return asset - }) -} - -export const getAssets = async (): Promise => { - const panoraAssets = await fetchPanoraTokens() + // Filter out the native APT token from CoinGecko to avoid duplicates + // CoinGecko includes native APT both as the slip44 base asset (added manually) and + // as the coin-standard token (0x1::aptos_coin::AptosCoin) at the same metadata + // address 0xa under the aptosCoin namespace. + const nativeAptCoinPattern = /^aptos:[^/]+\/aptosCoin:(0x0*a|0x1::aptos_coin::AptosCoin)$/i + const tokensOnly = assets.filter(asset => !nativeAptCoinPattern.test(asset.assetId)) - const allAssets = uniqBy(panoraAssets, 'assetId') + const allAssets = uniqBy(tokensOnly, 'assetId') return [unfreeze(aptos), ...allAssets] } diff --git a/scripts/generateAssetData/coingecko.ts b/scripts/generateAssetData/coingecko.ts index ad4aff4e95f..51ff1b08a1c 100644 --- a/scripts/generateAssetData/coingecko.ts +++ b/scripts/generateAssetData/coingecko.ts @@ -448,7 +448,11 @@ export async function getAssets(chainId: ChainId): Promise { case aptosChainId: return { assetNamespace: ASSET_NAMESPACE.aptosCoin, - category: adapters.chainIdToCoingeckoAssetPlatform(chainId), + // CoinGecko uses two different platform slugs for Aptos: their /api/v3 + // endpoints (and our adapter) use 'aptos-network', but the static tokenlist + // at tokens.coingecko.com only responds to 'aptos' (the 'aptos-network' + // slug returns 403 there). Hardcoded to keep the token fetch working. + category: 'aptos', explorer: aptos.explorer, explorerAddressLink: aptos.explorerAddressLink, explorerTxLink: aptos.explorerTxLink, From f007bc773cef6c43415f5c924fecb4080d763aea Mon Sep 17 00:00:00 2001 From: Discostu Date: Thu, 9 Jul 2026 09:57:51 +0700 Subject: [PATCH 13/16] chore(deps): regenerate pnpm-lock for aptos deps after rebase --- pnpm-lock.yaml | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24f9b386bef..3c023408855 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -832,6 +832,9 @@ importers: packages/chain-adapters: dependencies: + '@aptos-labs/ts-sdk': + specifier: 6.3.1 + version: 6.3.1(got@11.8.6) '@mysten/sui': specifier: 1.45.2 version: 1.45.2(typescript@5.8.2) @@ -2090,6 +2093,9 @@ importers: packages/swapper: dependencies: + '@aptos-labs/ts-sdk': + specifier: 6.3.1 + version: 6.3.1(got@11.8.6) '@arbitrum/sdk': specifier: ^4.0.1 version: 4.0.4(bufferutil@4.1.0)(utf-8-validate@5.0.10) @@ -2361,6 +2367,20 @@ packages: peerDependencies: zod: 3.25.76 + '@aptos-labs/aptos-cli@1.1.1': + resolution: {integrity: sha512-sB7CokCM6s76SLJmccysbnFR+MDik6udKfj2+9ZsmTLV0/t73veIeCDKbvWJmbW267ibx4HiGbPI7L+1+yjEbQ==} + hasBin: true + + '@aptos-labs/aptos-client@2.2.0': + resolution: {integrity: sha512-lYgHI8ehgD+Ykhix0IwzLaTCknHp1KNmExbq2bPZk8IeTwQg79D5BOkD46MjW0jGbJbl+J/RBtVF9vM7Te/hWA==} + engines: {node: '>=20.0.0'} + peerDependencies: + got: ^11.8.6 + + '@aptos-labs/ts-sdk@6.3.1': + resolution: {integrity: sha512-1C13IaHgNIo6MHMTQEcDzeuqTNv++evdY+Ph3IAGLnHlG7Yevdxw50W0nyAJRf4bLzQDI20wfJ66wD2qMg7Rew==} + engines: {node: '>=20.0.0'} + '@arbitrum/sdk@4.0.4': resolution: {integrity: sha512-GscwlkHYmPzRKs9huDHntbqx1xMRhTraTUvTC9exu+prjndKxHe9ZORuIcqmtEqwLwma/l8nqxI+k+pEEdIO6Q==} engines: {node: '>=v11', npm: please-use-yarn, yarn: '>= 1.0.0'} @@ -17105,6 +17125,29 @@ snapshots: zod: 3.25.76 zod-to-json-schema: 3.25.1(zod@3.25.76) + '@aptos-labs/aptos-cli@1.1.1': + dependencies: + commander: 12.1.0 + + '@aptos-labs/aptos-client@2.2.0(got@11.8.6)': + dependencies: + got: 11.8.6 + + '@aptos-labs/ts-sdk@6.3.1(got@11.8.6)': + dependencies: + '@aptos-labs/aptos-cli': 1.1.1 + '@aptos-labs/aptos-client': 2.2.0(got@11.8.6) + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + eventemitter3: 5.0.4 + js-base64: 3.7.8 + jwt-decode: 4.0.0 + poseidon-lite: 0.2.1 + transitivePeerDependencies: + - got + '@arbitrum/sdk@4.0.4(bufferutil@4.1.0)(utf-8-validate@5.0.10)': dependencies: '@ethersproject/address': 5.8.0 From fb88d714b8e9f04095b5b12241834f34e4efed61 Mon Sep 17 00:00:00 2001 From: Discostu Date: Thu, 9 Jul 2026 10:05:51 +0700 Subject: [PATCH 14/16] fix(aptos): register aptos chain type in public-api chain list --- packages/public-api/src/routes/chains/types.ts | 1 + packages/public-api/src/routes/chains/utils.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/public-api/src/routes/chains/types.ts b/packages/public-api/src/routes/chains/types.ts index 29717b72c49..b4f9793608d 100644 --- a/packages/public-api/src/routes/chains/types.ts +++ b/packages/public-api/src/routes/chains/types.ts @@ -12,6 +12,7 @@ export const ChainTypeSchema = z.enum([ 'near', 'starknet', 'ton', + 'aptos', ]) export const ChainSchema = registry.register( diff --git a/packages/public-api/src/routes/chains/utils.ts b/packages/public-api/src/routes/chains/utils.ts index f6b3655b1bb..2d6416808ee 100644 --- a/packages/public-api/src/routes/chains/utils.ts +++ b/packages/public-api/src/routes/chains/utils.ts @@ -15,6 +15,7 @@ const chainNamespaceToType: Record = { [CHAIN_NAMESPACE.Near]: 'near', [CHAIN_NAMESPACE.Starknet]: 'starknet', [CHAIN_NAMESPACE.Ton]: 'ton', + [CHAIN_NAMESPACE.Aptos]: 'aptos', } const buildChainList = (): Chain[] => { From a6b12c9ac849af58430a453875908b9c454437c8 Mon Sep 17 00:00:00 2001 From: Discostu Date: Fri, 15 May 2026 00:04:06 +0200 Subject: [PATCH 15/16] feat: register PanoraSwapper with feature flag and CSP (web-ohh.12) --- .env | 3 + .env.development | 1 + headers/csps/defi/swappers/Panora.ts | 5 + headers/csps/index.ts | 2 + packages/public-api/src/config.ts | 1 + packages/public-api/src/env.ts | 1 + packages/swapper/src/constants.ts | 9 ++ packages/swapper/src/index.ts | 1 + .../swapperApi/getPanoraTradeData.ts | 140 ++++++++++++++++++ .../PanoraSwapper/swapperApi/getTradeQuote.ts | 121 +++++++++++++++ .../PanoraSwapper/swapperApi/getTradeRate.ts | 100 +++++++++++++ .../src/swappers/PanoraSwapper/types.ts | 39 +++++ .../swappers/PanoraSwapper/utils/constants.ts | 13 ++ .../swappers/PanoraSwapper/utils/helpers.ts | 26 ++++ .../PanoraSwapper/utils/panoraService.ts | 24 +++ packages/swapper/src/types.ts | 2 + .../components/SwapperIcon/SwapperIcon.tsx | 3 + .../components/SwapperIcon/panora-icon.jpg | Bin 0 -> 4726 bytes src/config.ts | 2 + src/state/helpers.ts | 4 + .../preferencesSlice/preferencesSlice.ts | 2 + src/test/mocks/store.ts | 1 + src/vite-env.d.ts | 1 + 23 files changed, 501 insertions(+) create mode 100644 headers/csps/defi/swappers/Panora.ts create mode 100644 packages/swapper/src/swappers/PanoraSwapper/swapperApi/getPanoraTradeData.ts create mode 100644 packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeQuote.ts create mode 100644 packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeRate.ts create mode 100644 packages/swapper/src/swappers/PanoraSwapper/types.ts create mode 100644 packages/swapper/src/swappers/PanoraSwapper/utils/constants.ts create mode 100644 packages/swapper/src/swappers/PanoraSwapper/utils/helpers.ts create mode 100644 packages/swapper/src/swappers/PanoraSwapper/utils/panoraService.ts create mode 100644 src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/panora-icon.jpg diff --git a/.env b/.env index 67920560f3e..a7548ac1045 100644 --- a/.env +++ b/.env @@ -373,3 +373,6 @@ VITE_AGENTIC_SERVER_BASE_URL=https://api.agent.shapeshift.com # bob gateway VITE_BOB_GATEWAY_API_KEY=fc969b70f1767095a1b188c92e30d7d9 +# panora (public API key from the Panora docs) +VITE_PANORA_API_KEY="a4^KV_EaTf4MW#ZdvgGKX#HUD^3IFEAOV_kzpIE^3BQGA8pDnrkT7JcIy#HNlLGi" +VITE_FEATURE_PANORA_SWAP=false diff --git a/.env.development b/.env.development index 68b71b87427..517da83c7fa 100644 --- a/.env.development +++ b/.env.development @@ -109,3 +109,4 @@ VITE_PROXY_API_BASE_URL=https://dev-api.proxy.shapeshift.com # Agentic Chat # VITE_AGENTIC_SERVER_BASE_URL=http://localhost:4111 +VITE_FEATURE_PANORA_SWAP=true diff --git a/headers/csps/defi/swappers/Panora.ts b/headers/csps/defi/swappers/Panora.ts new file mode 100644 index 00000000000..e2b062a6a52 --- /dev/null +++ b/headers/csps/defi/swappers/Panora.ts @@ -0,0 +1,5 @@ +import type { Csp } from '../../types' + +export const csp: Csp = { + 'connect-src': ['https://api.panora.exchange'], +} diff --git a/headers/csps/index.ts b/headers/csps/index.ts index 6407791749b..71567bfbc54 100644 --- a/headers/csps/index.ts +++ b/headers/csps/index.ts @@ -68,6 +68,7 @@ import { csp as butterSwap } from './defi/swappers/ButterSwap' import { csp as cowSwap } from './defi/swappers/CowSwap' import { csp as nearIntents } from './defi/swappers/NearIntents' import { csp as oneInch } from './defi/swappers/OneInch' +import { csp as panora } from './defi/swappers/Panora' import { csp as portals } from './defi/swappers/Portals' import { csp as stonfi } from './defi/swappers/Stonfi' import { csp as sunio } from './defi/swappers/Sunio' @@ -210,4 +211,5 @@ export const csps = [ railway, discord, yieldxyz, + panora, ] diff --git a/packages/public-api/src/config.ts b/packages/public-api/src/config.ts index 20d15324e04..3d168d12aa2 100644 --- a/packages/public-api/src/config.ts +++ b/packages/public-api/src/config.ts @@ -39,4 +39,5 @@ export const getServerConfig = (): SwapperConfig => ({ VITE_ACROSS_INTEGRATOR_ID: env.ACROSS_INTEGRATOR_ID, VITE_DEBRIDGE_API_URL: env.DEBRIDGE_API_URL, VITE_BOB_GATEWAY_API_KEY: env.BOB_GATEWAY_API_KEY, + VITE_PANORA_API_KEY: env.PANORA_API_KEY, }) diff --git a/packages/public-api/src/env.ts b/packages/public-api/src/env.ts index df897d2ab22..b0a473ff169 100644 --- a/packages/public-api/src/env.ts +++ b/packages/public-api/src/env.ts @@ -80,6 +80,7 @@ const envSchema = z.object({ ACROSS_INTEGRATOR_ID: z.string().default(''), BEBOP_API_KEY: z.string().min(1), BOB_GATEWAY_API_KEY: z.string().default(''), + PANORA_API_KEY: z.string().default(''), CHAINFLIP_API_KEY: z.string().min(1), NEAR_INTENTS_API_KEY: z.string().min(1), TENDERLY_API_KEY: z.string().min(1), diff --git a/packages/swapper/src/constants.ts b/packages/swapper/src/constants.ts index 3f403ffc6c9..29cc691d87e 100644 --- a/packages/swapper/src/constants.ts +++ b/packages/swapper/src/constants.ts @@ -24,6 +24,8 @@ import { mayachainApi } from './swappers/MayachainSwapper/endpoints' import { mayachainSwapper } from './swappers/MayachainSwapper/MayachainSwapper' import { nearIntentsApi } from './swappers/NearIntentsSwapper/endpoints' import { nearIntentsSwapper } from './swappers/NearIntentsSwapper/NearIntentsSwapper' +import { panoraApi } from './swappers/PanoraSwapper/endpoints' +import { panoraSwapper } from './swappers/PanoraSwapper/PanoraSwapper' import { portalsApi } from './swappers/PortalsSwapper/endpoints' import { portalsSwapper } from './swappers/PortalsSwapper/PortalsSwapper' import { relaySwapper } from './swappers/RelaySwapper' @@ -122,6 +124,10 @@ export const swappers: Record = ...bobGatewaySwapper, ...bobGatewayApi, }, + [SwapperName.Panora]: { + ...panoraSwapper, + ...panoraApi, + }, [SwapperName.Test]: undefined, } @@ -142,6 +148,7 @@ const DEFAULT_STONFI_SLIPPAGE_DECIMAL_PERCENTAGE = '0.01' // deBridge API off-chain simulation overestimates output on some chains (e.g. SEI ~2.4%), so auto slippage (1%) is insufficient const DEFAULT_DEBRIDGE_SLIPPAGE_DECIMAL_PERCENTAGE = '0.03' const DEFAULT_BOB_GATEWAY_SLIPPAGE_DECIMAL_PERCENTAGE = '0.03' +const DEFAULT_PANORA_SLIPPAGE_DECIMAL_PERCENTAGE = '0.005' export const getDefaultSlippageDecimalPercentageForSwapper = ( swapperName: SwapperName | undefined, @@ -170,6 +177,8 @@ export const getDefaultSlippageDecimalPercentageForSwapper = ( throw new Error('Default slippage not supported by Across') case SwapperName.Debridge: return DEFAULT_DEBRIDGE_SLIPPAGE_DECIMAL_PERCENTAGE + case SwapperName.Panora: + return DEFAULT_PANORA_SLIPPAGE_DECIMAL_PERCENTAGE case SwapperName.ButterSwap: return DEFAULT_BUTTERSWAP_SLIPPAGE_DECIMAL_PERCENTAGE case SwapperName.NearIntents: diff --git a/packages/swapper/src/index.ts b/packages/swapper/src/index.ts index 8bdd67512d0..f6dbc0fb5f2 100644 --- a/packages/swapper/src/index.ts +++ b/packages/swapper/src/index.ts @@ -11,6 +11,7 @@ export * from './swappers/ChainflipSwapper' export * from './swappers/SunioSwapper' export * from './swappers/CowSwapper' export * from './swappers/DebridgeSwapper' +export * from './swappers/PanoraSwapper' export * from './swappers/PortalsSwapper' export * from './swappers/ThorchainSwapper' export * from './swappers/MayachainSwapper' diff --git a/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getPanoraTradeData.ts b/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getPanoraTradeData.ts new file mode 100644 index 00000000000..4fcd1b7bbf6 --- /dev/null +++ b/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getPanoraTradeData.ts @@ -0,0 +1,140 @@ +import { bnOrZero } from '@shapeshiftoss/utils' +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' + +import type { AssetId } from '@shapeshiftoss/caip' +import type { Asset } from '@shapeshiftoss/types' +import type { ProtocolFee, SwapErrorRight, SwapperConfig } from '../../../types' +import { TradeQuoteError } from '../../../types' +import { getInputOutputRate, makeSwapErrorRight } from '../../../utils' +import { isSupportedChainId } from '../utils/constants' +import { getTokenAddress } from '../utils/helpers' +import { getPanoraService } from '../utils/panoraService' +import type { PanoraSwapResponse } from '../types' + +type PanoraTradeDataInput = { + sellAsset: Asset + buyAsset: Asset + sellAmountIncludingProtocolFeesCryptoBaseUnit: string + receiveAddress: string + affiliateBps: string | undefined + slippagePercentage: number + config: SwapperConfig +} + +type PanoraTradeData = { + buyAmountAfterFeesCryptoBaseUnit: string + rate: string + protocolFees: Record + txData: { + function: string + type_arguments: string[] + arguments: string[] + } + feeTokenAmount: string + priceImpact: number +} + +export const getPanoraTradeData = async ( + input: PanoraTradeDataInput, +): Promise> => { + const { + sellAsset, + buyAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + receiveAddress, + affiliateBps, + slippagePercentage, + config, + } = input + + if (!isSupportedChainId(sellAsset.chainId)) { + return Err( + makeSwapErrorRight({ + message: `unsupported chainId`, + code: TradeQuoteError.UnsupportedChain, + details: { chainId: sellAsset.chainId }, + }), + ) + } + + if (!isSupportedChainId(buyAsset.chainId)) { + return Err( + makeSwapErrorRight({ + message: `unsupported chainId`, + code: TradeQuoteError.UnsupportedChain, + details: { chainId: buyAsset.chainId }, + }), + ) + } + + const fromTokenAddress = getTokenAddress(sellAsset) + const toTokenAddress = getTokenAddress(buyAsset) + + // Convert affiliateBps (basis points) to integratorFeePercentage + // e.g. 30 bps = 0.3% + const integratorFeePercentage = + affiliateBps !== undefined ? bnOrZero(affiliateBps).div(100).toNumber() : undefined + + const requestBody: Record = { + fromTokenAddress, + toTokenAddress, + fromTokenAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit, + toWalletAddress: receiveAddress, + slippagePercentage, + } + + if (integratorFeePercentage !== undefined && integratorFeePercentage > 0) { + requestBody.integratorFeePercentage = integratorFeePercentage + } + + try { + const panoraService = getPanoraService(config.VITE_PANORA_API_KEY) + const responseResult = await panoraService.post('/swap', requestBody) + + if (responseResult.isErr()) { + return Err(responseResult.unwrapErr()) + } + + const { data } = responseResult.unwrap() + + if (!data?.quotes || data.quotes.length === 0) { + return Err( + makeSwapErrorRight({ + message: `No quotes returned from Panora for ${sellAsset.symbol}/${buyAsset.symbol}`, + code: TradeQuoteError.NoRouteFound, + }), + ) + } + + // Take the best quote (first one) + const bestQuote = data.quotes[0] + + const buyAmountAfterFeesCryptoBaseUnit = bestQuote.toTokenAmount + + const rate = getInputOutputRate({ + sellAmountCryptoBaseUnit: sellAmountIncludingProtocolFeesCryptoBaseUnit, + buyAmountCryptoBaseUnit: buyAmountAfterFeesCryptoBaseUnit, + sellAsset, + buyAsset, + }) + + const protocolFees: Record = {} + + return Ok({ + buyAmountAfterFeesCryptoBaseUnit, + rate, + protocolFees, + txData: bestQuote.txData, + feeTokenAmount: bestQuote.feeTokenAmount, + priceImpact: bestQuote.priceImpact, + }) + } catch (error) { + return Err( + makeSwapErrorRight({ + message: error instanceof Error ? error.message : 'Unknown error getting Panora data', + code: TradeQuoteError.QueryFailed, + }), + ) + } +} diff --git a/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeQuote.ts new file mode 100644 index 00000000000..e23c400d8db --- /dev/null +++ b/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeQuote.ts @@ -0,0 +1,121 @@ +import { bnOrZero } from '@shapeshiftoss/utils' +import type { Result } from '@sniptt/monads' +import { Err } from '@sniptt/monads' +import { v4 as uuid } from 'uuid' + +import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' +import type { CommonTradeQuoteInput, SwapErrorRight, SwapperDeps, TradeQuote } from '../../../types' +import { SwapperName, TradeQuoteError } from '../../../types' +import { makeSwapErrorRight } from '../../../utils' +import { buildAffiliateFee } from '../../utils/affiliateFee' +import { getPanoraTradeData } from './getPanoraTradeData' + +export const getTradeQuote = async ( + input: CommonTradeQuoteInput, + deps: SwapperDeps, +): Promise> => { + const { + sellAsset, + buyAsset, + receiveAddress, + accountNumber, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount, + affiliateBps, + slippageTolerancePercentageDecimal, + } = input + + if (accountNumber === undefined) { + return Err( + makeSwapErrorRight({ + message: `accountNumber is required`, + code: TradeQuoteError.InternalError, + }), + ) + } + + if (!receiveAddress) { + return Err( + makeSwapErrorRight({ + message: `receiveAddress is required`, + code: TradeQuoteError.InternalError, + }), + ) + } + + const slippagePercentage = bnOrZero( + slippageTolerancePercentageDecimal ?? + getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Panora), + ) + .times(100) + .toNumber() + + const tradeDataResult = await getPanoraTradeData({ + sellAsset, + buyAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount, + receiveAddress, + affiliateBps, + slippagePercentage, + config: deps.config, + }) + + if (tradeDataResult.isErr()) return Err(tradeDataResult.unwrapErr()) + + const { buyAmountAfterFeesCryptoBaseUnit, rate, protocolFees } = tradeDataResult.unwrap() + + try { + // Fixed gas estimate for Aptos transactions (100 octas per gas unit * ~2000 gas units) + const estimatedGas = '200000' + + const tradeQuote: TradeQuote = { + id: uuid(), + quoteOrRate: 'quote', + rate, + affiliateBps, + receiveAddress, + slippageTolerancePercentageDecimal, + swapperName: SwapperName.Panora, + steps: [ + { + accountNumber, + buyAmountBeforeFeesCryptoBaseUnit: buyAmountAfterFeesCryptoBaseUnit, + buyAmountAfterFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount, + feeData: { + protocolFees, + networkFeeCryptoBaseUnit: estimatedGas, + chainSpecific: { + gasUnitPrice: '100', + maxGasAmount: '2000', + }, + }, + rate, + source: SwapperName.Panora, + buyAsset, + sellAsset, + allowanceContract: '0x0', + estimatedExecutionTimeMs: undefined, + affiliateFee: buildAffiliateFee({ + strategy: 'buy_asset', + affiliateBps, + sellAsset, + buyAsset, + sellAmountCryptoBaseUnit: sellAmount, + buyAmountCryptoBaseUnit: buyAmountAfterFeesCryptoBaseUnit, + isEstimate: true, + }), + }, + ], + } + + return tradeDataResult.map(() => [tradeQuote]) + } catch (error) { + console.error(error) + return Err( + makeSwapErrorRight({ + message: error instanceof Error ? error.message : 'Unknown error getting Panora quote', + code: TradeQuoteError.QueryFailed, + }), + ) + } +} diff --git a/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeRate.ts new file mode 100644 index 00000000000..7a21c929fb6 --- /dev/null +++ b/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeRate.ts @@ -0,0 +1,100 @@ +import { bnOrZero } from '@shapeshiftoss/utils' +import type { Result } from '@sniptt/monads' +import { Err } from '@sniptt/monads' +import { v4 as uuid } from 'uuid' + +import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' +import type { GetTradeRateInput, SwapErrorRight, SwapperDeps, TradeRate } from '../../../types' +import { SwapperName, TradeQuoteError } from '../../../types' +import { makeSwapErrorRight } from '../../../utils' +import { buildAffiliateFee } from '../../utils/affiliateFee' +import { getPanoraTradeData } from './getPanoraTradeData' + +const DUMMY_WALLET_ADDRESS = '0x0000000000000000000000000000000000000000000000000000000000000000' + +export const getTradeRate = async ( + input: GetTradeRateInput, + deps: SwapperDeps, +): Promise> => { + const { + sellAsset, + buyAsset, + receiveAddress, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount, + affiliateBps, + slippageTolerancePercentageDecimal, + } = input + + const slippagePercentage = bnOrZero( + slippageTolerancePercentageDecimal ?? + getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Panora), + ) + .times(100) + .toNumber() + + const walletAddress = receiveAddress ?? DUMMY_WALLET_ADDRESS + + const tradeDataResult = await getPanoraTradeData({ + sellAsset, + buyAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount, + receiveAddress: walletAddress, + affiliateBps, + slippagePercentage, + config: deps.config, + }) + + if (tradeDataResult.isErr()) return Err(tradeDataResult.unwrapErr()) + + const { buyAmountAfterFeesCryptoBaseUnit, rate, protocolFees } = tradeDataResult.unwrap() + + try { + const estimatedGas = '200000' + + const tradeRate: TradeRate = { + id: uuid(), + quoteOrRate: 'rate', + rate, + affiliateBps, + receiveAddress, + slippageTolerancePercentageDecimal, + swapperName: SwapperName.Panora, + steps: [ + { + accountNumber: undefined, + buyAmountBeforeFeesCryptoBaseUnit: buyAmountAfterFeesCryptoBaseUnit, + buyAmountAfterFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount, + feeData: { + protocolFees, + networkFeeCryptoBaseUnit: estimatedGas, + }, + rate, + source: SwapperName.Panora, + buyAsset, + sellAsset, + allowanceContract: '0x0', + estimatedExecutionTimeMs: undefined, + affiliateFee: buildAffiliateFee({ + strategy: 'buy_asset', + affiliateBps, + sellAsset, + buyAsset, + sellAmountCryptoBaseUnit: sellAmount, + buyAmountCryptoBaseUnit: buyAmountAfterFeesCryptoBaseUnit, + isEstimate: true, + }), + }, + ], + } + + return tradeDataResult.map(() => [tradeRate]) + } catch (error) { + return Err( + makeSwapErrorRight({ + message: error instanceof Error ? error.message : 'Unknown error getting Panora rate', + code: TradeQuoteError.QueryFailed, + }), + ) + } +} diff --git a/packages/swapper/src/swappers/PanoraSwapper/types.ts b/packages/swapper/src/swappers/PanoraSwapper/types.ts new file mode 100644 index 00000000000..fdf0da0bbcf --- /dev/null +++ b/packages/swapper/src/swappers/PanoraSwapper/types.ts @@ -0,0 +1,39 @@ +export type PanoraTokenInfo = { + token_address: string + name: string + symbol: string + decimals: number + current_price: number | null +} + +export type PanoraSwapRequest = { + fromTokenAddress: string + toTokenAddress: string + fromTokenAmount: string + toWalletAddress: string + slippagePercentage: number + integratorFeePercentage?: number + integratorFeeAddress?: string +} + +export type PanoraTxData = { + function: string + type_arguments: string[] + arguments: string[] +} + +export type PanoraQuote = { + toTokenAmount: string + feeTokenAmount: string + feePercentage: number + priceImpact: number + slippagePercentage: number + txData: PanoraTxData +} + +export type PanoraSwapResponse = { + fromToken: string + toToken: string + feeToken: string + quotes: PanoraQuote[] +} diff --git a/packages/swapper/src/swappers/PanoraSwapper/utils/constants.ts b/packages/swapper/src/swappers/PanoraSwapper/utils/constants.ts new file mode 100644 index 00000000000..656109cf263 --- /dev/null +++ b/packages/swapper/src/swappers/PanoraSwapper/utils/constants.ts @@ -0,0 +1,13 @@ +import type { ChainId } from '@shapeshiftoss/caip' +import { aptosChainId } from '@shapeshiftoss/caip' + +export const SUPPORTED_PANORA_CHAIN_IDS: readonly ChainId[] = [aptosChainId] + +export const isSupportedChainId = ( + chainId: ChainId, +): chainId is (typeof SUPPORTED_PANORA_CHAIN_IDS)[number] => { + return SUPPORTED_PANORA_CHAIN_IDS.includes(chainId) +} + +export const PANORA_NATIVE_TOKEN_ADDRESS = '0xa' +export const PANORA_DEFAULT_SLIPPAGE_DECIMAL_PERCENTAGE = '0.005' diff --git a/packages/swapper/src/swappers/PanoraSwapper/utils/helpers.ts b/packages/swapper/src/swappers/PanoraSwapper/utils/helpers.ts new file mode 100644 index 00000000000..049e54e0f4c --- /dev/null +++ b/packages/swapper/src/swappers/PanoraSwapper/utils/helpers.ts @@ -0,0 +1,26 @@ +import { fromAssetId } from '@shapeshiftoss/caip' +import type { Asset } from '@shapeshiftoss/types' + +import { PANORA_NATIVE_TOKEN_ADDRESS } from './constants' + +export const getTokenAddress = (asset: Asset): string => { + const { assetNamespace, assetReference } = fromAssetId(asset.assetId) + + // For native APT (slip44 namespace), return the Panora native token address + if (assetNamespace === 'slip44') { + return PANORA_NATIVE_TOKEN_ADDRESS + } + + // For tokens, the assetReference is the Aptos token address + return assetReference +} + +export const aptosAddressFromAssetId = (assetId: string): string => { + const { assetReference, assetNamespace } = fromAssetId(assetId) + + if (assetNamespace === 'slip44') { + return PANORA_NATIVE_TOKEN_ADDRESS + } + + return assetReference +} diff --git a/packages/swapper/src/swappers/PanoraSwapper/utils/panoraService.ts b/packages/swapper/src/swappers/PanoraSwapper/utils/panoraService.ts new file mode 100644 index 00000000000..953e2e71351 --- /dev/null +++ b/packages/swapper/src/swappers/PanoraSwapper/utils/panoraService.ts @@ -0,0 +1,24 @@ +import { createCache, makeSwapperAxiosServiceMonadic } from '../../../utils' + +const PANORA_API_BASE = 'https://api.panora.exchange' + +type PanoraService = ReturnType + +let cachedService: PanoraService | undefined +let cachedApiKey: string | undefined + +export const getPanoraService = (apiKey: string): PanoraService => { + if (!cachedService || cachedApiKey !== apiKey) { + const service = createCache(5000, ['/swap'], { + baseURL: PANORA_API_BASE, + timeout: 10000, + headers: { + Accept: 'application/json', + 'x-api-key': apiKey, + }, + }) + cachedService = makeSwapperAxiosServiceMonadic(service) + cachedApiKey = apiKey + } + return cachedService +} diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index 756fd13f7c1..06d6347a3c6 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -90,6 +90,7 @@ export type SwapperConfig = { VITE_ACROSS_INTEGRATOR_ID: string VITE_DEBRIDGE_API_URL: string VITE_BOB_GATEWAY_API_KEY: string + VITE_PANORA_API_KEY: string } export enum SwapperName { @@ -112,6 +113,7 @@ export enum SwapperName { Across = 'Across', Debridge = 'deBridge', BobGateway = 'BOB Gateway', + Panora = 'Panora', } export type SwapSource = SwapperName | `${SwapperName} • ${string}` diff --git a/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/SwapperIcon.tsx b/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/SwapperIcon.tsx index ef1a1027435..901e1454fff 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/SwapperIcon.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/SwapperIcon.tsx @@ -15,6 +15,7 @@ import CowIcon from './cow-icon.png' import DebridgeIcon from './debridge-icon.svg' import MayachainIcon from './maya_logo.png' import NearIntentsIcon from './near-intents-icon.png' +import PanoraIcon from './panora-icon.jpg' import PortalsIcon from './portals-icon.png' import RelayIcon from './relay-icon.svg' import StonfiIcon from './stonfi-icon.png' @@ -69,6 +70,8 @@ export const SwapperIcon = ({ return DebridgeIcon case SwapperName.BobGateway: return BobGatewayIcon + case SwapperName.Panora: + return PanoraIcon case SwapperName.Test: return '' default: diff --git a/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/panora-icon.jpg b/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/panora-icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e4d418feb7b47118cf3eadedba6c3c3668013e8a GIT binary patch literal 4726 zcmbVPXIN8Nw>}{WO$5|16j6qrC{-dLD%B`ShLRvv$|ouklrBh7nv^jry-1`42n3{A z!XS!(RA~|df*^vRfPgfSj>$bR^Ua<4albqF?&sO(Jm>7a*Sp@m)?Rxp&O6R~AQIr^ z=H}ss^YZZU@bU5T3m|q2?Aj$DzIUGx;(&zI!2=SKl0VC-DE=&?Br7S2)Iuq#siV=F>$y<6;vw50Tr|8wKi0eg5M00e+SWB{%`5a=EVrxB71 z01!B2rvZO72p1Fv=jP$%p*u$<6ngpa1oMz@S@^QPDBK-XUn)bV^ed>>((|_Pw(5l{&xd|qhp^wk55c~nVMeww)A~@Wp!aWOk|2$<8xfarOOD4tw*aiDrCE zU}EUaTAdTEdNm`Nnq=Vm&R||g(&p@Xk1LhupA8RytJ4d*|Gu2^3xNz%-I0gh5u<@6 zBX`}I!2Vd->O6Qcd&IMYMIdvHt#50fab{;Ctx1duUvgpxX-&8;$+0&>t_>0NTmG36 z|CxdhR$+PleR=5_Q?r^{f3EZHB0gs5Z5e)7=0WF+-G24N1_?mOa5=!zpwg;dCn z_zVtkv!>vIJ{&;+JCO;%RgifIUUfVf<*grampGOhZVzsioxS88x*r zzeyQ{2l+K|?Irtz0Geb!G4Khha@SmRKN;tT5O9}9d?lJ6zgN)nx`3&U;>|~!n-3yo z$lQp}n={LZL7$t;8)QIgzOuK6)V2D4n#2HzOq8oIza1P&H4MjNkkr&V0!Eu3CPt?b zwcAd2oQ%Gbv*37J@M%eckJh=mtPACQbf zax&vW2m4bUyGv_pF^Rp7uD$$#9ni`FZctV`dVS4As!fzwO5TO-8X1tap2v01$Yy4j z*Em4k38}A3ph_Z5_hzD`>x1=EeRFSY4m|n9-*H@!U7O-DQFMAD zMWc;Du|(O{>aFoaJP3^0dJ?+%xi_GN;J9fycXigI-@TKeA%qpyTB3h#Iw;*gOy7Y~ zL|7M2XTf?@-cq{!XfJ=;dL2UL0Jx7w-32#KKe#kxb6OiKn(Udwj?RCT_qKilt*&D* zu;Su@0xf*VOy6Bzn~dYuj3;~3sHD3zx`dH|k%=^&D8Vg%On2i4Ml&P(W=L-Mt^amY zv^f{68;6v$BN(H4`J$A$QX?=320xtZczN^+ZpPt_r^?%NhOX!y z>tT#_+i`#bdqK6B&lYndqmIGFGlS8U3Nux;{(@F=qoND!f~WeWnNkfBu}_SeL{mJ6 zN!m^g{R??kL1-@ z6Zc+kbY#D5Nx&SdmthyQEB(yD^P@bVMY67lHe?}R=x~6oUsUXj%W6zbMfR_G=|J8u zKAa@`ABnd9HAT^~_#E9TNY|}mHnS$jZKYrRwv2ZKef@{q4VFtKLECxAp>HyNEyf_t zsODR%D)e)cV&Qk)vB#ak3YGaSB_yB0XZv4wTn&wSA+b0m>q>L|fP0=uJJ@I~Z+9c^ z^`_EOiA8j&>TOT<#cR>Y%qz0J;#gA}T7X2OTbRJ53*vk3$wT-59w5*=#|R;`eRSYL z?8h|>ewebV8_tglL1gh043xa`5y`m`n0!P;22!{pC;v<3VD+#=20Yq}b#PAzeT&k+ zV0Gtm@reEY-pj@!wiep3q>*psW@O~Y+s@HPV~>hi8ieWbv(#$nx9?AkSSRJf14DR@ z=B>$^_0+ff!#~tbPD;uA5Uu^>pa;>10@q61of$Tki5gNOzy~Ph2l0}1;uU^;vK6WMR?UvJdx<~B3mE!Q`cU^6|rEHM~ zH%FP?#=ocGp4-l^rSE4EgV=%LYuODQv(U6@JsnUaa>%4VyG{zhLBG2338 zF8$WVEr*q;hh^pY7~hury0-dX8_L(%M_acH2rDyMa~IC9OR-;yuu@bmdZzdin}d%9 z=G{dne=;e9PB)^XlUe5KjOYwfK}7r@&d$idP~z-%1dWaapHx?C4q!84XMp1XGyA7W zNNyzkIt`Im%7wT_=QqG1F^2Q z$}V0T+gL+e=?@wP3<=uDm3uZUm6==K!5_af)u{QX`iayif$eSCF?-R=3~ul?fJ~n6 zy5y{sh*gwZEgtkp2!7c9gvIS8)G-<5R8&@ZW!CyjIGr`I&=Llh#Jrmxh~JLyO}%JKktR+u?E4tpQ)% zJN|7}`f|f5b5{#)^}FL9niX`>76S#}j~lkbs86ga!2?$uB4UbJn2w}R6`S*`Jc zduh6DIeDlJ{0a@u2~q~ zI``tfz446XA+d*kS2tIt98)e|xI(X&W`xU_VI;GQ=Is^3=A;=(=9EApvJc&UtZv$E zrSwyyc{j;>-XrVUE$G4la5LJT7d(a7u*`D*?nT$aVb-bY*@R( zfS=;GwDQClr4Tx`tc}&ySHDN3gl})?6P+JsLC*th@wTofOt0Y5Uf`|QB%4z{7RD<* zPn!3r!AT-FvOwebc{Tju!sQr380vG6uVa*sdQ5#BSB*^R^`N^%60*$Ke$lcfz@fmjDuGo00EZM_oWenz z0a#KU7lUz9@TSjrF8S0wS>Q#bHV^Z9`6k+(haOaQNMKd-ocUjga=%^R1L-YBk5ot;y z5fl=ZR-`H1Lo$(v3bTk5BmG8n58l`n#WzeLk&s9uLoiKck}T%Sd(G8CYep9gW<)PE_j*e!N&ii5%Tsr|}X>@vSc$9P=I+Ce|jwXXD)6hh7wE~I-*1*^Zi;<=} zOYCIV?mLMf{BM;Cg4lym=8{BVK;IC`M=(jQ^0E2cl-1W&h zAyc@Fs1}1|4#59ucVmZ|OZ=^Oj|-Pfe>?g{vdC_%e*Dlq43YRUrrV@bylmgQ0~jr| zOPDA+oz{r}s%Sfm6cWL-mOGJb=qh0-VT3oKQbBeSf2$g}cak2)?}h+SBqWuGV7yxy zcE^-E9j_n5n%JiEXd1<+q8dFtP13+{9iX5XE@9xWs@$+gRe9)SmoR>m23EhRQyvk? zM2As4TxrxClkE>RbTUD|ekXZ;@IO_0_AG+FU_N4q#WbGQFv;%(j-wfa8u~E=$}im% z`1t|QVt?X1s-n?4#i>fl3ZNp;M=s=F!0ViZq$z}l?;wGtAJ>0I+paCQ5qIHDI#pbS z84M6tnnYrlL#y%>l6E%G7faF5--X3u02NR)=p*vtv`(n#p8)?1 { case SwapperName.Test: case SwapperName.Avnu: case SwapperName.Cetus: + case SwapperName.Panora: // Technically supported for Arbitrum Bridge, but we disable it for the sake of simplicity for now return false default: @@ -57,6 +58,7 @@ export const getEnabledSwappers = ( AcrossSwap, DebridgeSwap, BobGatewaySwap, + PanoraSwap, }: FeatureFlags, isCrossAccountTrade: boolean, walletName?: string, @@ -117,6 +119,8 @@ export const getEnabledSwappers = ( [SwapperName.BobGateway]: BobGatewaySwap && (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.BobGateway)), + [SwapperName.Panora]: + PanoraSwap && (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.Panora)), [SwapperName.Test]: false, } } diff --git a/src/state/slices/preferencesSlice/preferencesSlice.ts b/src/state/slices/preferencesSlice/preferencesSlice.ts index d5352eedcaf..9bc08a29325 100644 --- a/src/state/slices/preferencesSlice/preferencesSlice.ts +++ b/src/state/slices/preferencesSlice/preferencesSlice.ts @@ -127,6 +127,7 @@ export type FeatureFlags = { AcrossSwap: boolean DebridgeSwap: boolean BobGatewaySwap: boolean + PanoraSwap: boolean LazyTxHistory: boolean LedgerReadOnly: boolean QuickBuy: boolean @@ -302,6 +303,7 @@ const initialState: Preferences = { AcrossSwap: getConfig().VITE_FEATURE_ACROSS_SWAP, DebridgeSwap: getConfig().VITE_FEATURE_DEBRIDGE_SWAP, BobGatewaySwap: getConfig().VITE_FEATURE_BOB_GATEWAY_SWAP, + PanoraSwap: getConfig().VITE_FEATURE_PANORA_SWAP, LazyTxHistory: getConfig().VITE_FEATURE_TX_HISTORY_BYE_BYE, LedgerReadOnly: getConfig().VITE_FEATURE_LEDGER_READ_ONLY, QuickBuy: getConfig().VITE_FEATURE_QUICK_BUY, diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index 28cba6096fc..b3ab52ccdaf 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -198,6 +198,7 @@ export const mockStore: ReduxState = { AcrossSwap: false, DebridgeSwap: false, BobGatewaySwap: false, + PanoraSwap: false, LazyTxHistory: false, QuickBuy: false, NewWalletManager: false, diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index d7a35caaae9..469944f7b4d 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -223,6 +223,7 @@ interface ImportMetaEnv { readonly VITE_FEATURE_SEI: string readonly VITE_FEATURE_NOTIFICATIONS_WEBSERVICES: string readonly VITE_FEATURE_APTOS: string + readonly VITE_FEATURE_PANORA_SWAP: string readonly VITE_APTOS_NODE_URL: string readonly VITE_APTOS_INDEXER_URL: string From e013ce3ce91978de70932a2afae76105ed83bb2e Mon Sep 17 00:00:00 2001 From: Discostu Date: Fri, 15 May 2026 00:13:21 +0200 Subject: [PATCH 16/16] feat: add PanoraSwapper implementation (web-ohh.11) --- .../src/aptos/AptosChainAdapter.ts | 31 +++-- .../swappers/PanoraSwapper/PanoraSwapper.ts | 6 + .../src/swappers/PanoraSwapper/endpoints.ts | 116 ++++++++++++++++++ .../src/swappers/PanoraSwapper/index.ts | 5 + .../swapperApi/getPanoraTradeData.ts | 27 ++-- .../PanoraSwapper/swapperApi/getTradeQuote.ts | 11 +- .../PanoraSwapper/swapperApi/getTradeRate.ts | 11 +- .../swappers/PanoraSwapper/utils/constants.ts | 7 ++ 8 files changed, 190 insertions(+), 24 deletions(-) create mode 100644 packages/swapper/src/swappers/PanoraSwapper/PanoraSwapper.ts create mode 100644 packages/swapper/src/swappers/PanoraSwapper/endpoints.ts create mode 100644 packages/swapper/src/swappers/PanoraSwapper/index.ts diff --git a/packages/chain-adapters/src/aptos/AptosChainAdapter.ts b/packages/chain-adapters/src/aptos/AptosChainAdapter.ts index 3498079c30f..7aa700617fe 100644 --- a/packages/chain-adapters/src/aptos/AptosChainAdapter.ts +++ b/packages/chain-adapters/src/aptos/AptosChainAdapter.ts @@ -234,18 +234,14 @@ export class ChainAdapter implements IChainAdapter { return recommended > MIN_MAX_GAS_AMOUNT ? recommended : MIN_MAX_GAS_AMOUNT } - async buildSendApiTransaction( - input: BuildSendApiTxInput, - ): Promise> { + async buildEntryFunctionApiTransaction(input: { + from: string + accountNumber: number + data: InputEntryFunctionData + }): Promise> { try { - const { from, accountNumber, to, value, chainSpecific } = input - const coinType = chainSpecific?.coinType ?? APT_COIN_TYPE + const { from, accountNumber, data } = input - const data: InputEntryFunctionData = { - function: '0x1::aptos_account::transfer_coins', - typeArguments: [coinType], - functionArguments: [to, BigInt(value)], - } const maxGasAmount = Number(await this.estimateMaxGasAmount(from, data)) const transaction = await this.client.transaction.build.simple({ @@ -269,6 +265,21 @@ export class ChainAdapter implements IChainAdapter { } } + buildSendApiTransaction( + input: BuildSendApiTxInput, + ): Promise> { + const { from, accountNumber, to, value, chainSpecific } = input + const coinType = chainSpecific?.coinType ?? APT_COIN_TYPE + + const data: InputEntryFunctionData = { + function: '0x1::aptos_account::transfer_coins', + typeArguments: [coinType], + functionArguments: [to, BigInt(value)], + } + + return this.buildEntryFunctionApiTransaction({ from, accountNumber, data }) + } + async buildSendTransaction(input: BuildSendTxInput): Promise<{ txToSign: SignTx }> { diff --git a/packages/swapper/src/swappers/PanoraSwapper/PanoraSwapper.ts b/packages/swapper/src/swappers/PanoraSwapper/PanoraSwapper.ts new file mode 100644 index 00000000000..9d860591fbf --- /dev/null +++ b/packages/swapper/src/swappers/PanoraSwapper/PanoraSwapper.ts @@ -0,0 +1,6 @@ +import { executeAptosTransaction } from '../..' +import type { Swapper } from '../../types' + +export const panoraSwapper: Swapper = { + executeAptosTransaction, +} diff --git a/packages/swapper/src/swappers/PanoraSwapper/endpoints.ts b/packages/swapper/src/swappers/PanoraSwapper/endpoints.ts new file mode 100644 index 00000000000..373515131e8 --- /dev/null +++ b/packages/swapper/src/swappers/PanoraSwapper/endpoints.ts @@ -0,0 +1,116 @@ +import { TxStatus } from '@shapeshiftoss/unchained-client' +import { bnOrZero } from '@shapeshiftoss/utils' +import type { Result } from '@sniptt/monads' + +import { getDefaultSlippageDecimalPercentageForSwapper } from '../../constants' +import type { + CommonTradeQuoteInput, + GetTradeRateInput, + GetUnsignedAptosTransactionArgs, + SwapErrorRight, + SwapperApi, + SwapperDeps, + TradeQuote, + TradeRate, + TradeStatus, +} from '../../types' +import { SwapperName } from '../../types' +import { checkAptosSwapStatus, isExecutableTradeQuote } from '../../utils' +import { getPanoraTradeData } from './swapperApi/getPanoraTradeData' +import { getTradeQuote } from './swapperApi/getTradeQuote' +import { getTradeRate } from './swapperApi/getTradeRate' + +export const panoraApi: SwapperApi = { + getTradeQuote: ( + input: CommonTradeQuoteInput, + deps: SwapperDeps, + ): Promise> => { + return getTradeQuote(input, deps) + }, + + getTradeRate: ( + input: GetTradeRateInput, + deps: SwapperDeps, + ): Promise> => { + return getTradeRate(input, deps) + }, + + getUnsignedAptosTransaction: async ({ + stepIndex, + tradeQuote, + from, + assertGetAptosChainAdapter, + config, + }: GetUnsignedAptosTransactionArgs) => { + if (!isExecutableTradeQuote(tradeQuote)) throw new Error('Unable to execute a trade rate quote') + + const step = tradeQuote.steps[stepIndex ?? 0] + if (!step) throw new Error(`No step at index ${stepIndex}`) + + const { accountNumber, sellAsset, buyAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit } = + step + + const adapter = assertGetAptosChainAdapter(sellAsset.chainId) + + // Re-fetch quote from Panora to get fresh txData for execution + const slippagePercentage = bnOrZero( + tradeQuote.slippageTolerancePercentageDecimal ?? + getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Panora), + ) + .times(100) + .toNumber() + + const tradeDataResult = await getPanoraTradeData({ + sellAsset, + buyAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + receiveAddress: from, + affiliateBps: tradeQuote.affiliateBps, + slippagePercentage, + config, + }) + + if (tradeDataResult.isErr()) { + throw new Error(`Failed to get Panora trade data: ${tradeDataResult.unwrapErr().message}`) + } + + const { txData } = tradeDataResult.unwrap() + + return adapter.buildEntryFunctionApiTransaction({ + from, + accountNumber: accountNumber ?? 0, + data: { + function: txData.function as `${string}::${string}::${string}`, + typeArguments: txData.type_arguments, + functionArguments: txData.arguments, + }, + }) + }, + + getAptosTransactionFees: ({ tradeQuote, stepIndex }: GetUnsignedAptosTransactionArgs) => { + if (!isExecutableTradeQuote(tradeQuote)) throw new Error('Unable to execute a trade rate quote') + + const step = tradeQuote.steps[stepIndex ?? 0] + if (!step) throw new Error('Missing step') + if (!step.feeData.networkFeeCryptoBaseUnit) { + throw new Error('Missing network fee in quote') + } + return Promise.resolve(step.feeData.networkFeeCryptoBaseUnit) + }, + + checkTradeStatus: ({ swap, assertGetAptosChainAdapter }): Promise => { + if (!swap?.sellTxHash) { + return Promise.resolve({ + status: TxStatus.Unknown, + buyTxHash: undefined, + message: undefined, + }) + } + + return checkAptosSwapStatus({ + txHash: swap.sellTxHash, + address: swap.receiveAddress, + assertGetAptosChainAdapter, + }) + }, +} diff --git a/packages/swapper/src/swappers/PanoraSwapper/index.ts b/packages/swapper/src/swappers/PanoraSwapper/index.ts new file mode 100644 index 00000000000..39cc30c8982 --- /dev/null +++ b/packages/swapper/src/swappers/PanoraSwapper/index.ts @@ -0,0 +1,5 @@ +export { + PANORA_NATIVE_TOKEN_ADDRESS, + PANORA_DEFAULT_SLIPPAGE_DECIMAL_PERCENTAGE, + SUPPORTED_PANORA_CHAIN_IDS, +} from './utils/constants' diff --git a/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getPanoraTradeData.ts b/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getPanoraTradeData.ts index 4fcd1b7bbf6..123f3f79e79 100644 --- a/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getPanoraTradeData.ts +++ b/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getPanoraTradeData.ts @@ -1,16 +1,16 @@ -import { bnOrZero } from '@shapeshiftoss/utils' +import type { AssetId } from '@shapeshiftoss/caip' +import type { Asset } from '@shapeshiftoss/types' +import { bnOrZero, fromBaseUnit, toBaseUnit } from '@shapeshiftoss/utils' import type { Result } from '@sniptt/monads' import { Err, Ok } from '@sniptt/monads' -import type { AssetId } from '@shapeshiftoss/caip' -import type { Asset } from '@shapeshiftoss/types' import type { ProtocolFee, SwapErrorRight, SwapperConfig } from '../../../types' import { TradeQuoteError } from '../../../types' import { getInputOutputRate, makeSwapErrorRight } from '../../../utils' -import { isSupportedChainId } from '../utils/constants' +import type { PanoraSwapResponse } from '../types' +import { isSupportedChainId, PANORA_INTEGRATOR_FEE_ADDRESS } from '../utils/constants' import { getTokenAddress } from '../utils/helpers' import { getPanoraService } from '../utils/panoraService' -import type { PanoraSwapResponse } from '../types' type PanoraTradeDataInput = { sellAsset: Asset @@ -79,13 +79,24 @@ export const getPanoraTradeData = async ( const requestBody: Record = { fromTokenAddress, toTokenAddress, - fromTokenAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit, + // The Panora API deals in human-readable token amounts, not base units + fromTokenAmount: fromBaseUnit( + sellAmountIncludingProtocolFeesCryptoBaseUnit, + sellAsset.precision, + ), toWalletAddress: receiveAddress, slippagePercentage, } - if (integratorFeePercentage !== undefined && integratorFeePercentage > 0) { + // Panora deducts integratorFeePercentage from the user even without integratorFeeAddress, + // but only routes it on-chain when the address is present. Never send the percentage alone. + if ( + PANORA_INTEGRATOR_FEE_ADDRESS && + integratorFeePercentage !== undefined && + integratorFeePercentage > 0 + ) { requestBody.integratorFeePercentage = integratorFeePercentage + requestBody.integratorFeeAddress = PANORA_INTEGRATOR_FEE_ADDRESS } try { @@ -110,7 +121,7 @@ export const getPanoraTradeData = async ( // Take the best quote (first one) const bestQuote = data.quotes[0] - const buyAmountAfterFeesCryptoBaseUnit = bestQuote.toTokenAmount + const buyAmountAfterFeesCryptoBaseUnit = toBaseUnit(bestQuote.toTokenAmount, buyAsset.precision) const rate = getInputOutputRate({ sellAmountCryptoBaseUnit: sellAmountIncludingProtocolFeesCryptoBaseUnit, diff --git a/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeQuote.ts index e23c400d8db..cab9d414256 100644 --- a/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeQuote.ts @@ -8,6 +8,7 @@ import type { CommonTradeQuoteInput, SwapErrorRight, SwapperDeps, TradeQuote } f import { SwapperName, TradeQuoteError } from '../../../types' import { makeSwapErrorRight } from '../../../utils' import { buildAffiliateFee } from '../../utils/affiliateFee' +import { PANORA_INTEGRATOR_FEE_ADDRESS } from '../utils/constants' import { getPanoraTradeData } from './getPanoraTradeData' export const getTradeQuote = async ( @@ -49,12 +50,16 @@ export const getTradeQuote = async ( .times(100) .toNumber() + // No integrator fee address is configured yet, so Panora cannot route an affiliate fee + // to the DAO; zero it out rather than displaying a fee that is not collected. + const appliedAffiliateBps = PANORA_INTEGRATOR_FEE_ADDRESS ? affiliateBps : '0' + const tradeDataResult = await getPanoraTradeData({ sellAsset, buyAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount, receiveAddress, - affiliateBps, + affiliateBps: appliedAffiliateBps, slippagePercentage, config: deps.config, }) @@ -71,7 +76,7 @@ export const getTradeQuote = async ( id: uuid(), quoteOrRate: 'quote', rate, - affiliateBps, + affiliateBps: appliedAffiliateBps, receiveAddress, slippageTolerancePercentageDecimal, swapperName: SwapperName.Panora, @@ -97,7 +102,7 @@ export const getTradeQuote = async ( estimatedExecutionTimeMs: undefined, affiliateFee: buildAffiliateFee({ strategy: 'buy_asset', - affiliateBps, + affiliateBps: appliedAffiliateBps, sellAsset, buyAsset, sellAmountCryptoBaseUnit: sellAmount, diff --git a/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeRate.ts index 7a21c929fb6..f8f7b725416 100644 --- a/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeRate.ts +++ b/packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeRate.ts @@ -8,6 +8,7 @@ import type { GetTradeRateInput, SwapErrorRight, SwapperDeps, TradeRate } from ' import { SwapperName, TradeQuoteError } from '../../../types' import { makeSwapErrorRight } from '../../../utils' import { buildAffiliateFee } from '../../utils/affiliateFee' +import { PANORA_INTEGRATOR_FEE_ADDRESS } from '../utils/constants' import { getPanoraTradeData } from './getPanoraTradeData' const DUMMY_WALLET_ADDRESS = '0x0000000000000000000000000000000000000000000000000000000000000000' @@ -34,12 +35,16 @@ export const getTradeRate = async ( const walletAddress = receiveAddress ?? DUMMY_WALLET_ADDRESS + // No integrator fee address is configured yet, so Panora cannot route an affiliate fee + // to the DAO; zero it out rather than displaying a fee that is not collected. + const appliedAffiliateBps = PANORA_INTEGRATOR_FEE_ADDRESS ? affiliateBps : '0' + const tradeDataResult = await getPanoraTradeData({ sellAsset, buyAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount, receiveAddress: walletAddress, - affiliateBps, + affiliateBps: appliedAffiliateBps, slippagePercentage, config: deps.config, }) @@ -55,7 +60,7 @@ export const getTradeRate = async ( id: uuid(), quoteOrRate: 'rate', rate, - affiliateBps, + affiliateBps: appliedAffiliateBps, receiveAddress, slippageTolerancePercentageDecimal, swapperName: SwapperName.Panora, @@ -77,7 +82,7 @@ export const getTradeRate = async ( estimatedExecutionTimeMs: undefined, affiliateFee: buildAffiliateFee({ strategy: 'buy_asset', - affiliateBps, + affiliateBps: appliedAffiliateBps, sellAsset, buyAsset, sellAmountCryptoBaseUnit: sellAmount, diff --git a/packages/swapper/src/swappers/PanoraSwapper/utils/constants.ts b/packages/swapper/src/swappers/PanoraSwapper/utils/constants.ts index 656109cf263..ff4692dda72 100644 --- a/packages/swapper/src/swappers/PanoraSwapper/utils/constants.ts +++ b/packages/swapper/src/swappers/PanoraSwapper/utils/constants.ts @@ -11,3 +11,10 @@ export const isSupportedChainId = ( export const PANORA_NATIVE_TOKEN_ADDRESS = '0xa' export const PANORA_DEFAULT_SLIPPAGE_DECIMAL_PERCENTAGE = '0.005' + +// Panora routes integrator fees on-chain only when integratorFeeAddress accompanies +// integratorFeePercentage; the percentage alone is still deducted from the user but kept +// by Panora (verified against the live /swap API). Leave unset until the DAO has an Aptos +// treasury address (the DAO_TREASURY_APTOS placeholder was removed during review for the +// same reason). +export const PANORA_INTEGRATOR_FEE_ADDRESS: string | undefined = undefined