diff --git a/.env b/.env index 5f424cb4679..740cfec5976 100644 --- a/.env +++ b/.env @@ -119,6 +119,8 @@ VITE_FEATURE_DEBRIDGE_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 48d2f40d91b..c06e07f87cb 100644 --- a/.env.development +++ b/.env.development @@ -39,6 +39,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/.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 new file mode 100644 index 00000000000..2a630a344aa --- /dev/null +++ b/headers/csps/chains/aptos.ts @@ -0,0 +1,15 @@ +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, + env.VITE_APTOS_INDEXER_URL, + 'https://fullnode.mainnet.aptoslabs.com/', + 'https://api.mainnet.aptoslabs.com/', + ], +} diff --git a/headers/csps/index.ts b/headers/csps/index.ts index 93352adb685..c0fa60c22dc 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' @@ -130,6 +131,7 @@ export const csps = [ bitcoincash, blast, abstract, + aptos, bnbsmartchain, cosmos, dogecoin, 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 59983dcca4e..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, @@ -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..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, @@ -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' }, }, ) diff --git a/packages/caip/src/constants.ts b/packages/caip/src/constants.ts index 7bed0cf1c18..3eefffa7d77 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' @@ -142,6 +143,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', @@ -153,6 +155,7 @@ export const CHAIN_NAMESPACE = { Near: 'near', Starknet: 'starknet', Ton: 'ton', + Aptos: 'aptos', } as const type ValidChainMap = { @@ -210,6 +213,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 = { @@ -224,6 +228,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 = { @@ -277,6 +282,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({ @@ -336,6 +342,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 = { @@ -357,6 +364,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 @@ -407,4 +415,5 @@ export const FEE_ASSET_IDS = [ katanaAssetId, etherealAssetId, flowEvmAssetId, + aptosAssetId, ] diff --git a/packages/chain-adapters/package.json b/packages/chain-adapters/package.json index 951a74b67d9..fa32a1c6c5a 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..ef2cfd7b798 --- /dev/null +++ b/packages/chain-adapters/src/aptos/AptosChainAdapter.test.ts @@ -0,0 +1,310 @@ +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' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { ValidAddressResultType } from '../types' +import { ChainAdapter } from './AptosChainAdapter' + +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(), + 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('accepts 64-hex address without 0x prefix (Aptos SDK normalizes)', async () => { + const r = await adapter.validateAddress(ADDR.slice(2)) + expect(r).toEqual({ valid: true, result: ValidAddressResultType.Valid }) + }) + + 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', () => { + 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 new file mode 100644 index 00000000000..3498079c30f --- /dev/null +++ b/packages/chain-adapters/src/aptos/AptosChainAdapter.ts @@ -0,0 +1,570 @@ +import type { InputEntryFunctionData } from '@aptos-labs/ts-sdk' +import { + AccountAddress, + AccountAuthenticatorEd25519, + Aptos, + AptosConfig, + Deserializer, + Ed25519PublicKey, + Ed25519Signature, + Network, + SimpleTransaction, +} from '@aptos-labs/ts-sdk' +import type { AssetId, ChainId } from '@shapeshiftoss/caip' +import { + aptosAssetId, + aptosChainId, + ASSET_NAMESPACE, + ASSET_REFERENCE, + toAssetId, +} from '@shapeshiftoss/caip' +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' +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' +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 = 20_000n + +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 + 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 { + 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 + } + + getRpcUrl() { + return this.rpcUrl + } + + 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 balances = await this.client.getAccountCoinsData({ + accountAddress: pubkey, + }) + + let nativeBalance = '0' + const tokens: AptosToken[] = [] + + for (const entry of balances) { + if (!entry.asset_type || BigInt(entry.amount ?? 0) === 0n) continue + + 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: nativeBalance, + chainId: this.chainId, + assetId: this.assetId, + chain: this.getType(), + chainSpecific: { tokens }, + pubkey, + } + } catch (err) { + return ErrorHandler(err, { + translation: 'chainAdapters.errors.getAccount', + options: { pubkey }, + }) + } + } + + validateAddress(address: string): Promise { + try { + AccountAddress.fromString(address) + 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') + } + + 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 }, + }) + // 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 + } + + async buildSendApiTransaction( + input: BuildSendApiTxInput, + ): Promise> { + try { + 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)], + } + const maxGasAmount = Number(await this.estimateMaxGasAmount(from, data)) + + const transaction = await this.client.transaction.build.simple({ + sender: from, + data, + options: { maxGasAmount }, + }) + + const signingMessageBytes = this.client.getSigningMessage({ transaction }) + const rawTransactionBytes = transaction.bcsToBytes() + + return { + addressNList: toAddressNList(this.getBip44Params({ accountNumber })), + signingMessageBytes, + rawTransactionBytes, + } + } 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, + signingMessageBytes: txToSign.signingMessageBytes, + rawTransactionBytes: txToSign.rawTransactionBytes, + }) + + if (!signedTx?.signature || !signedTx?.publicKey) { + throw new Error('error signing tx - missing signature or publicKey') + } + + return JSON.stringify({ + signature: signedTx.signature, + publicKey: signedTx.publicKey, + rawTransactionBytes: Array.from(signedTx.rawTransactionBytes), + }) + } catch (err) { + return ErrorHandler(err, { + translation: 'chainAdapters.errors.signTransaction', + }) + } + } + + async signAndBroadcastTransaction({ + senderAddress, + receiverAddress, + signTxInput, + }: SignAndBroadcastTransactionInput): Promise { + try { + const signedTxHex = await this.signTransaction(signTxInput) + return this.broadcastTransaction({ senderAddress, receiverAddress, 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) as { + signature: string + publicKey: string + rawTransactionBytes: number[] + } + + 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', + }) + } + } + + async getFeeData( + input: GetFeeDataInput, + ): Promise> { + try { + 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: 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, { + translation: 'chainAdapters.errors.getFeeData', + }) + } + } + + subscribeTxs(): Promise { + return Promise.resolve() + } + + unsubscribeTxs(): void { + return + } + + closeTxs(): void { + 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 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 txid = tx.hash ?? (typeof txHashOrTx === 'string' ? txHashOrTx : '') + const blockHeight = Number(tx.version ?? 0) + const blockTime = tx.timestamp ? Math.floor(Number(tx.timestamp) / 1_000_000) : 0 + + // 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' + const fee = { + assetId: this.assetId, + value: (BigInt(gasUsed) * BigInt(gasUnitPrice)).toString(), + } + + const transfers: Transaction['transfers'] = [] + 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 ?? '' + + // 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], + to: [recipient], + type: TransferType.Send, + value: amount, + }) + } + if (eq(recipient, pubkey)) { + transfers.push({ + assetId: transferAssetId, + from: [sender], + to: [recipient], + type: TransferType.Receive, + value: 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', + }) + } + } +} diff --git a/packages/chain-adapters/src/aptos/index.ts b/packages/chain-adapters/src/aptos/index.ts new file mode 100644 index 00000000000..b63c7b8b23f --- /dev/null +++ b/packages/chain-adapters/src/aptos/index.ts @@ -0,0 +1,3 @@ +export { ChainAdapter } from './AptosChainAdapter' + +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..d5f708e9322 --- /dev/null +++ b/packages/chain-adapters/src/aptos/types.ts @@ -0,0 +1,36 @@ +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 + // Aptos CoinStore-style coin type to transfer (e.g. '0x1::aptos_coin::AptosCoin'). + // Defaults to APT when omitted. + coinType?: 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..5859d105a35 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, @@ -19,6 +20,7 @@ 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' @@ -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/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) diff --git a/packages/hdwallet-core/src/aptos.ts b/packages/hdwallet-core/src/aptos.ts new file mode 100644 index 00000000000..9af672c1eab --- /dev/null +++ b/packages/hdwallet-core/src/aptos.ts @@ -0,0 +1,115 @@ +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 + /** 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 + rawTransactionBytes: 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..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' @@ -405,6 +406,14 @@ 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 { return isObject(wallet) && (wallet as any)._isMetaMask } diff --git a/packages/hdwallet-native/src/aptos.ts b/packages/hdwallet-native/src/aptos.ts new file mode 100644 index 00000000000..d2d874120bf --- /dev/null +++ b/packages/hdwallet-native/src/aptos.ts @@ -0,0 +1,63 @@ +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) + } + + aptosNextAccountPath(msg: core.AptosAccountPath): core.AptosAccountPath | undefined { + return core.aptosNextAccountPath(msg) + } + } +} + +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.signingMessageBytes, + msg.addressNList, + ) + + return { + signature, + publicKey, + 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 new file mode 100644 index 00000000000..b575328d051 --- /dev/null +++ b/packages/hdwallet-native/src/crypto/isolation/adapters/aptos.ts @@ -0,0 +1,69 @@ +import * as core from '@shapeshiftoss/hdwallet-core' +import { createSHA3 } from 'hash-wasm' + +import type { Isolation } from '../..' + +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) + + 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 { + 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..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' @@ -133,15 +134,17 @@ class NativeHDWalletInfo MixinNativeStarknetWalletInfo( MixinNativeTronWalletInfo( MixinNativeTonWalletInfo( - MixinNativeSuiWalletInfo( - MixinNativeNearWalletInfo( - MixinNativeThorchainWalletInfo( - MixinNativeMayachainWalletInfo( - MixinNativeSecretWalletInfo( - MixinNativeTerraWalletInfo( - MixinNativeKavaWalletInfo( - MixinNativeArkeoWalletInfo( - MixinNativeOsmosisWalletInfo(NativeHDWalletBase), + MixinNativeAptosWalletInfo( + MixinNativeSuiWalletInfo( + MixinNativeNearWalletInfo( + MixinNativeThorchainWalletInfo( + MixinNativeMayachainWalletInfo( + MixinNativeSecretWalletInfo( + MixinNativeTerraWalletInfo( + MixinNativeKavaWalletInfo( + MixinNativeArkeoWalletInfo( + MixinNativeOsmosisWalletInfo(NativeHDWalletBase), + ), ), ), ), @@ -174,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()) { @@ -232,6 +236,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,14 +252,18 @@ export class NativeHDWallet MixinNativeStarknetWallet( MixinNativeTronWallet( MixinNativeTonWallet( - MixinNativeSuiWallet( - MixinNativeNearWallet( - MixinNativeThorchainWallet( - MixinNativeMayachainWallet( - MixinNativeSecretWallet( - MixinNativeTerraWallet( - MixinNativeKavaWallet( - MixinNativeOsmosisWallet(MixinNativeArkeoWallet(NativeHDWalletInfo)), + MixinNativeAptosWallet( + MixinNativeSuiWallet( + MixinNativeNearWallet( + MixinNativeThorchainWallet( + MixinNativeMayachainWallet( + MixinNativeSecretWallet( + MixinNativeTerraWallet( + MixinNativeKavaWallet( + MixinNativeOsmosisWallet( + MixinNativeArkeoWallet(NativeHDWalletInfo), + ), + ), ), ), ), @@ -285,7 +295,8 @@ export class NativeHDWallet core.TerraWallet, core.KavaWallet, core.OsmosisWallet, - core.ArkeoWallet + core.ArkeoWallet, + core.AptosWallet { readonly _isNative = true @@ -453,6 +464,7 @@ export class NativeHDWallet super.solanaInitializeWallet(ed25519MasterKey), super.suiInitializeWallet(ed25519MasterKey), super.nearInitializeWallet(ed25519MasterKey), + super.aptosInitializeWallet(ed25519MasterKey), ] if (this.#tonMasterKey) { @@ -517,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 f602083cdd6..c64428382ff 100644 --- a/packages/public-api/src/swapperDeps.ts +++ b/packages/public-api/src/swapperDeps.ts @@ -182,5 +182,8 @@ export const getSwapperDeps = (): SwapperDeps => ({ assertGetTonChainAdapter: notImplemented('Ton') as unknown as ( chainId: ChainId, ) => adapters.ton.ChainAdapter, + assertGetAptosChainAdapter: notImplemented('Aptos') as unknown as ( + chainId: ChainId, + ) => adapters.aptos.ChainAdapter, fetchIsSmartContractAddressQuery: () => Promise.resolve(false), }) diff --git a/packages/swapper/package.json b/packages/swapper/package.json index 51535083425..42f9affb4ea 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..b3af5603de5 100644 --- a/packages/swapper/src/swappers/NearIntentsSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/NearIntentsSwapper/endpoints.ts @@ -1,3 +1,4 @@ +import { ASSET_NAMESPACE, fromAssetId } from '@shapeshiftoss/caip' import { evm } from '@shapeshiftoss/chain-adapters' import type { EvmChainId } from '@shapeshiftoss/types' import { contractAddressOrUndefined } from '@shapeshiftoss/utils' @@ -5,6 +6,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 +379,50 @@ 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 + + // Native APT lives at the slip44 namespace; non-native Aptos coins encode + // their Move CoinStore type as the asset reference and must be passed + // through to the adapter so the typeArguments target the right coin. + const { assetNamespace, assetReference } = fromAssetId(sellAsset.assetId) + const chainSpecific = + assetNamespace === ASSET_NAMESPACE.slip44 ? {} : { coinType: assetReference } + + 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 = { [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 59766bef101..cb2efec5fac 100644 --- a/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts +++ b/packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts @@ -666,6 +666,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/types.ts b/packages/swapper/src/types.ts index c6e5288a76c..ea13f8f0ee9 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, @@ -171,6 +173,11 @@ export type SuiFeeData = { gasPrice: string } +export type AptosFeeData = { + gasUnitPrice: string + maxGasAmount: string +} + export type AmountDisplayMeta = { amountCryptoBaseUnit: string asset: Partial & Pick @@ -181,7 +188,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 = { @@ -370,6 +377,10 @@ export type TonSwapperDeps = { assertGetTonChainAdapter: (chainId: ChainId) => ton.ChainAdapter } +export type AptosSwapperDeps = { + assertGetAptosChainAdapter: (chainId: ChainId) => aptos.ChainAdapter +} + export type SwapperDeps = { assetsById: AssetsByIdPartial config: SwapperConfig @@ -383,7 +394,8 @@ export type SwapperDeps = { SuiSwapperDeps & NearSwapperDeps & StarknetSwapperDeps & - TonSwapperDeps + TonSwapperDeps & + AptosSwapperDeps export type AffiliateFee = { assetId: AssetId @@ -725,6 +737,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 } @@ -775,6 +791,11 @@ export type GetUnsignedTonTransactionArgs = CommonGetUnsignedTransactionArgs & TonAccountMetadata & TonSwapperDeps +type AptosAccountMetadata = { from: string } +export type GetUnsignedAptosTransactionArgs = CommonGetUnsignedTransactionArgs & + AptosAccountMetadata & + AptosSwapperDeps + export type GetUnsignedEvmMessageArgs = CommonGetUnsignedTransactionArgs & EvmAccountMetadata & Omit @@ -814,7 +835,8 @@ export type CheckTradeStatusInput = { SuiSwapperDeps & NearSwapperDeps & StarknetSwapperDeps & - TonSwapperDeps + TonSwapperDeps & + AptosSwapperDeps export type TradeStatus = { status: TxStatus @@ -883,6 +905,10 @@ export type Swapper = { txToSign: ton.TonSignTx, callbacks: TonTransactionExecutionProps, ) => Promise + executeAptosTransaction?: ( + txToSign: AptosSignTx, + callbacks: AptosTransactionExecutionProps, + ) => Promise } export type SwapperApi = { @@ -909,6 +935,7 @@ export type SwapperApi = { input: GetUnsignedStarknetTransactionArgs, ) => Promise getUnsignedTonTransaction?: (input: GetUnsignedTonTransactionArgs) => Promise + getUnsignedAptosTransaction?: (input: GetUnsignedAptosTransactionArgs) => Promise getEvmTransactionFees?: (input: GetUnsignedEvmTransactionArgs) => Promise getSolanaTransactionFees?: (input: GetUnsignedSolanaTransactionArgs) => Promise @@ -919,6 +946,7 @@ export type SwapperApi = { getNearTransactionFees?: (input: GetUnsignedNearTransactionArgs) => Promise getStarknetTransactionFees?: (input: GetUnsignedStarknetTransactionArgs) => Promise getTonTransactionFees?: (input: GetUnsignedTonTransactionArgs) => Promise + getAptosTransactionFees?: (input: GetUnsignedAptosTransactionArgs) => Promise } export type QuoteResult = Result & { @@ -980,6 +1008,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..55dca5a4e58 100644 --- a/packages/swapper/src/utils.ts +++ b/packages/swapper/src/utils.ts @@ -1,6 +1,8 @@ +import { AccountAddress } from '@aptos-labs/ts-sdk' 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 +13,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 { 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' @@ -23,6 +30,7 @@ import { setupCache } from 'axios-cache-interceptor' import { fetchSafeTransactionInfo } from './safe-utils' import type { + AptosTransactionExecutionProps, EvmTransactionExecutionProps, ExecutableTradeStep, NearTransactionExecutionProps, @@ -227,6 +235,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, @@ -503,6 +518,49 @@ 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) + + // 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.some(isTargetAddress), + ) + 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/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..6f888b165ef 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://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/', + relatedAssetKey: null, +}) diff --git a/packages/utils/src/assetData/getBaseAsset.ts b/packages/utils/src/assetData/getBaseAsset.ts index 33e613002c7..4c706217e30 100644 --- a/packages/utils/src/assetData/getBaseAsset.ts +++ b/packages/utils/src/assetData/getBaseAsset.ts @@ -5,6 +5,7 @@ import { KnownChainIds } from '@shapeshiftoss/types' import { assertUnreachable } from '../assertUnreachable' import { abstract, + aptos, arbitrum, atom, avax, @@ -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..e1b1c6fb490 100644 --- a/packages/utils/src/chainIdToFeeAssetId.ts +++ b/packages/utils/src/chainIdToFeeAssetId.ts @@ -1,6 +1,7 @@ import type { AssetId, ChainId } from '@shapeshiftoss/caip' import { abstractAssetId, + aptosAssetId, arbitrumAssetId, avalancheAssetId, baseAssetId, @@ -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) } 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/pnpm-lock.yaml b/pnpm-lock.yaml index 6cdb8f8724a..d97d03ec51e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -738,7 +738,7 @@ importers: version: 0.15.2(@babel/preset-env@7.29.0(@babel/core@7.29.0)) jsdom: specifier: ^28.0.0 - version: 28.0.0(@noble/hashes@2.0.1) + version: 28.0.0(@noble/hashes@2.2.0) limiter: specifier: ^2.1.0 version: 2.1.0 @@ -798,7 +798,7 @@ importers: version: 5.1.4(typescript@5.2.2)(vite@6.4.1(@types/node@22.19.13)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: 3.0.9 - version: 3.0.9(@types/debug@4.1.12)(@types/node@22.19.13)(happy-dom@20.7.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@28.0.0(@noble/hashes@2.0.1))(msw@0.36.8)(terser@5.46.0) + version: 3.0.9(@types/debug@4.1.12)(@types/node@22.19.13)(happy-dom@20.7.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@28.0.0(@noble/hashes@2.2.0))(msw@0.36.8)(terser@5.46.0) packages/affiliate-dashboard: dependencies: @@ -824,6 +824,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) @@ -1112,7 +1115,7 @@ importers: devDependencies: vitest: specifier: 3.0.9 - version: 3.0.9(@types/debug@4.1.12)(@types/node@25.3.5)(happy-dom@20.7.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@28.0.0(@noble/hashes@2.0.1))(msw@0.27.2)(terser@5.46.0) + version: 3.0.9(@types/debug@4.1.12)(@types/node@25.3.5)(happy-dom@20.7.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@28.0.0(@noble/hashes@2.2.0))(msw@0.27.2)(terser@5.46.0) packages/hdwallet-keepkey: dependencies: @@ -2052,7 +2055,7 @@ importers: version: 2.22.1(@tanstack/query-core@5.69.0)(@types/react@19.1.2)(immer@9.0.21)(react@19.2.4)(typescript@5.2.2)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.43.5(bufferutil@4.1.0)(typescript@5.2.2)(utf-8-validate@5.0.10)(zod@3.25.76)) jsdom: specifier: 28.0.0 - version: 28.0.0(@noble/hashes@2.0.1) + version: 28.0.0(@noble/hashes@2.2.0) react: specifier: ^19.0.0 version: 19.2.4 @@ -2076,13 +2079,16 @@ importers: version: 0.23.0(rollup@4.59.0)(vite@5.4.21(@types/node@22.19.13)(terser@5.46.0)) vitest: specifier: 4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.13)(happy-dom@20.7.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@28.0.0(@noble/hashes@2.0.1))(msw@0.36.8)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.13)(happy-dom@20.7.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@28.0.0(@noble/hashes@2.2.0))(msw@0.36.8)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) wagmi: specifier: 3.3.2 version: 3.3.2(c4dcca76a1369be4275944c3506dc32f) 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) @@ -2351,6 +2357,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'} @@ -4993,8 +5013,8 @@ packages: resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} engines: {node: ^14.21.3 || >=16} - '@noble/curves@2.0.1': - resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==} + '@noble/curves@2.2.0': + resolution: {integrity: sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==} engines: {node: '>= 20.19.0'} '@noble/ed25519@1.7.5': @@ -5042,8 +5062,8 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} - '@noble/hashes@2.0.1': - resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + '@noble/hashes@2.2.0': + resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==} engines: {node: '>= 20.19.0'} '@noble/secp256k1@1.7.1': @@ -17179,6 +17199,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 @@ -19340,8 +19383,8 @@ snapshots: '@ethereumjs/common': 10.1.1 '@ethereumjs/rlp': 10.1.1 '@ethereumjs/util': 10.1.1 - '@noble/curves': 2.0.1 - '@noble/hashes': 2.0.1 + '@noble/curves': 2.2.0 + '@noble/hashes': 2.2.0 '@ethereumjs/tx@3.5.2': dependencies: @@ -19365,8 +19408,8 @@ snapshots: '@ethereumjs/util@10.1.1': dependencies: '@ethereumjs/rlp': 10.1.1 - '@noble/curves': 2.0.1 - '@noble/hashes': 2.0.1 + '@noble/curves': 2.2.0 + '@noble/hashes': 2.2.0 '@ethereumjs/util@8.1.0': dependencies: @@ -19953,9 +19996,9 @@ snapshots: dependencies: '@exodus/bitcoin-wallet-standard-core': 0.0.0 - '@exodus/bytes@1.14.1(@noble/hashes@2.0.1)': + '@exodus/bytes@1.14.1(@noble/hashes@2.2.0)': optionalDependencies: - '@noble/hashes': 2.0.1 + '@noble/hashes': 2.2.0 '@fastify/busboy@2.1.1': {} @@ -22110,9 +22153,9 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 - '@noble/curves@2.0.1': + '@noble/curves@2.2.0': dependencies: - '@noble/hashes': 2.0.1 + '@noble/hashes': 2.2.0 '@noble/ed25519@1.7.5': {} @@ -22138,7 +22181,7 @@ snapshots: '@noble/hashes@1.8.0': {} - '@noble/hashes@2.0.1': {} + '@noble/hashes@2.2.0': {} '@noble/secp256k1@1.7.1': {} @@ -28932,7 +28975,7 @@ snapshots: '@trezor/device-authenticity@1.1.2(tslib@2.8.1)': dependencies: - '@noble/curves': 2.0.1 + '@noble/curves': 2.2.0 '@trezor/crypto-utils': 1.2.0(tslib@2.8.1) '@trezor/protobuf': 1.5.2(tslib@2.8.1) '@trezor/schema-utils': 1.4.0(tslib@2.8.1) @@ -34944,10 +34987,10 @@ snapshots: data-uri-to-buffer@6.0.2: {} - data-urls@7.0.0(@noble/hashes@2.0.1): + data-urls@7.0.0(@noble/hashes@2.2.0): dependencies: whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1(@noble/hashes@2.0.1) + whatwg-url: 16.0.1(@noble/hashes@2.2.0) transitivePeerDependencies: - '@noble/hashes' @@ -37158,9 +37201,9 @@ snapshots: domhandler: 5.0.3 htmlparser2: 10.1.0 - html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1): + html-encoding-sniffer@6.0.0(@noble/hashes@2.2.0): dependencies: - '@exodus/bytes': 1.14.1(@noble/hashes@2.0.1) + '@exodus/bytes': 1.14.1(@noble/hashes@2.2.0) transitivePeerDependencies: - '@noble/hashes' @@ -37827,15 +37870,15 @@ snapshots: jsdoc-type-pratt-parser@4.1.0: {} - jsdom@28.0.0(@noble/hashes@2.0.1): + jsdom@28.0.0(@noble/hashes@2.2.0): dependencies: '@acemir/cssom': 0.9.31 '@asamuzakjp/dom-selector': 6.8.1 - '@exodus/bytes': 1.14.1(@noble/hashes@2.0.1) + '@exodus/bytes': 1.14.1(@noble/hashes@2.2.0) cssstyle: 5.3.7 - data-urls: 7.0.0(@noble/hashes@2.0.1) + data-urls: 7.0.0(@noble/hashes@2.2.0) decimal.js: 10.6.0 - html-encoding-sniffer: 6.0.0(@noble/hashes@2.0.1) + html-encoding-sniffer: 6.0.0(@noble/hashes@2.2.0) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 @@ -37847,7 +37890,7 @@ snapshots: w3c-xmlserializer: 5.0.0 webidl-conversions: 8.0.1 whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1(@noble/hashes@2.0.1) + whatwg-url: 16.0.1(@noble/hashes@2.2.0) xml-name-validator: 5.0.0 transitivePeerDependencies: - '@noble/hashes' @@ -42860,7 +42903,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitest@3.0.9(@types/debug@4.1.12)(@types/node@22.19.13)(happy-dom@20.7.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@28.0.0(@noble/hashes@2.0.1))(msw@0.36.8)(terser@5.46.0): + vitest@3.0.9(@types/debug@4.1.12)(@types/node@22.19.13)(happy-dom@20.7.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@28.0.0(@noble/hashes@2.2.0))(msw@0.36.8)(terser@5.46.0): dependencies: '@vitest/expect': 3.0.9 '@vitest/mocker': 3.0.9(msw@0.36.8)(vite@5.4.21(@types/node@22.19.13)(terser@5.46.0)) @@ -42886,7 +42929,7 @@ snapshots: '@types/debug': 4.1.12 '@types/node': 22.19.13 happy-dom: 20.7.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - jsdom: 28.0.0(@noble/hashes@2.0.1) + jsdom: 28.0.0(@noble/hashes@2.2.0) transitivePeerDependencies: - less - lightningcss @@ -42898,7 +42941,7 @@ snapshots: - supports-color - terser - vitest@3.0.9(@types/debug@4.1.12)(@types/node@25.3.5)(happy-dom@20.7.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@28.0.0(@noble/hashes@2.0.1))(msw@0.27.2)(terser@5.46.0): + vitest@3.0.9(@types/debug@4.1.12)(@types/node@25.3.5)(happy-dom@20.7.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(jsdom@28.0.0(@noble/hashes@2.2.0))(msw@0.27.2)(terser@5.46.0): dependencies: '@vitest/expect': 3.0.9 '@vitest/mocker': 3.0.9(msw@0.27.2)(vite@5.4.21(@types/node@25.3.5)(terser@5.46.0)) @@ -42924,7 +42967,7 @@ snapshots: '@types/debug': 4.1.12 '@types/node': 25.3.5 happy-dom: 20.7.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - jsdom: 28.0.0(@noble/hashes@2.0.1) + jsdom: 28.0.0(@noble/hashes@2.2.0) transitivePeerDependencies: - less - lightningcss @@ -42936,7 +42979,7 @@ snapshots: - supports-color - terser - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.13)(happy-dom@20.7.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@28.0.0(@noble/hashes@2.0.1))(msw@0.36.8)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.13)(happy-dom@20.7.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@28.0.0(@noble/hashes@2.2.0))(msw@0.36.8)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 '@vitest/mocker': 4.0.18(msw@0.36.8)(vite@6.4.1(@types/node@22.19.13)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) @@ -42962,7 +43005,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@types/node': 22.19.13 happy-dom: 20.7.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) - jsdom: 28.0.0(@noble/hashes@2.0.1) + jsdom: 28.0.0(@noble/hashes@2.2.0) transitivePeerDependencies: - jiti - less @@ -43449,9 +43492,9 @@ snapshots: whatwg-mimetype@5.0.0: {} - whatwg-url@16.0.1(@noble/hashes@2.0.1): + whatwg-url@16.0.1(@noble/hashes@2.2.0): dependencies: - '@exodus/bytes': 1.14.1(@noble/hashes@2.0.1) + '@exodus/bytes': 1.14.1(@noble/hashes@2.2.0) tr46: 6.0.0 webidl-conversions: 8.0.1 transitivePeerDependencies: diff --git a/scripts/generateAssetData/aptos/index.ts b/scripts/generateAssetData/aptos/index.ts new file mode 100644 index 00000000000..8be615e2687 --- /dev/null +++ b/scripts/generateAssetData/aptos/index.ts @@ -0,0 +1,27 @@ +import { aptosChainId } from '@shapeshiftoss/caip' +import type { Asset } from '@shapeshiftoss/types' +import { aptos, unfreeze } from '@shapeshiftoss/utils' +import uniqBy from 'lodash/uniqBy' + +import * as coingecko from '../coingecko' + +export const getAssets = async (): Promise => { + const results = await Promise.allSettled([coingecko.getAssets(aptosChainId)]) + + const [assets] = results.map(result => { + if (result.status === 'fulfilled') return result.value + console.error('Error fetching Aptos assets from CoinGecko:', result.reason) + return [] + }) + + // 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(tokensOnly, 'assetId') + + return [unfreeze(aptos), ...allAssets] +} diff --git a/scripts/generateAssetData/coingecko.ts b/scripts/generateAssetData/coingecko.ts index 8c83dbfeab8..51ff1b08a1c 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,18 @@ export async function getAssets(chainId: ChainId): Promise { explorerAddressLink: ton.explorerAddressLink, explorerTxLink: ton.explorerTxLink, } + case aptosChainId: + return { + assetNamespace: ASSET_NAMESPACE.aptosCoin, + // 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, + } 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 2cc5e8d96dd..2235631eee0 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, @@ -37,6 +38,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' @@ -784,6 +786,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 f6ef1ae4e95..0ef714bc490 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteOrRateInput.ts +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteOrRateInput.ts @@ -297,6 +297,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 662cb529474..0317d741c75 100644 --- a/src/config.ts +++ b/src/config.ts @@ -302,6 +302,9 @@ 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_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/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..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, @@ -58,6 +59,7 @@ import { isPhantom, isVultisig, supportsAbstract, + supportsAptos, supportsArbitrum, supportsAvalanche, supportsBase, @@ -224,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: @@ -321,6 +324,8 @@ export const walletSupportsChain = ({ return supportsTron(wallet) case suiChainId: return supportsSui(wallet) + case aptosChainId: + 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 b7a4af6d59b..8ac4bee698c 100644 --- a/src/lib/account/account.ts +++ b/src/lib/account/account.ts @@ -4,6 +4,7 @@ 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 +27,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 +59,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/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 fc4ebda605e..5e67f17637d 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' @@ -105,6 +107,7 @@ export const fetchTradeStatus = async ({ assertGetSuiChainAdapter, assertGetNearChainAdapter, assertGetStarknetChainAdapter, + assertGetAptosChainAdapter, fetchIsSmartContractAddressQuery, }) @@ -927,4 +930,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 new file mode 100644 index 00000000000..a7fe3293d06 --- /dev/null +++ b/src/lib/utils/aptos.ts @@ -0,0 +1,51 @@ +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 + 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 = ( + 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.getRpcUrl() + + 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/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 new file mode 100644 index 00000000000..2d6658dbaae --- /dev/null +++ b/src/plugins/aptos/index.tsx @@ -0,0 +1,31 @@ +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, + 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 5c02c9ef1c3..5bf2e03fe0b 100644 --- a/src/state/slices/opportunitiesSlice/mappings.ts +++ b/src/state/slices/opportunitiesSlice/mappings.ts @@ -217,6 +217,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/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: [] }, + ) + } } diff --git a/src/state/slices/preferencesSlice/preferencesSlice.ts b/src/state/slices/preferencesSlice/preferencesSlice.ts index 898fad5e6f8..14754959a54 100644 --- a/src/state/slices/preferencesSlice/preferencesSlice.ts +++ b/src/state/slices/preferencesSlice/preferencesSlice.ts @@ -139,6 +139,7 @@ export type FeatureFlags = { YieldMultiAccount: boolean EarnTab: boolean MmNativeMultichain: boolean + Aptos: boolean } export type Flag = keyof FeatureFlags @@ -312,6 +313,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 8ee38716dcf..e7453498969 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..d7a35caaae9 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -222,6 +222,9 @@ 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 + readonly VITE_APTOS_INDEXER_URL: string // Only present in *some* envs readonly VITE_MIXPANEL_TOKEN?: string