Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 58 additions & 3 deletions packages/chain-adapters/src/aptos/AptosChainAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import type * as AptosSdkActual from '@aptos-labs/ts-sdk'
import { Deserializer, RawTransaction } from '@aptos-labs/ts-sdk'
import {
AccountAddress,
ChainId,
Deserializer,
generateRawTransaction,
Hex,
RawTransaction,
} from '@aptos-labs/ts-sdk'
import { aptosAssetId } from '@shapeshiftoss/caip'
import { TransferType, TxStatus } from '@shapeshiftoss/unchained-client'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { ValidAddressResultType } from '../types'
import { ChainAdapter } from './AptosChainAdapter'
import { getSerializedTransferPayload } from './utils'

vi.mock('@aptos-labs/ts-sdk', async () => {
// keep the real bcs and key classes, mock only the network client
Expand Down Expand Up @@ -39,6 +47,7 @@ vi.mock('@aptos-labs/ts-sdk', async () => {
}
}),
AptosConfig: vi.fn(),
generateRawTransaction: vi.fn(),
Network: { MAINNET: 'mainnet' },
}
})
Expand Down Expand Up @@ -247,6 +256,44 @@ describe('AptosChainAdapter', () => {
})
})

describe('buildCustomApiTx', () => {
it.each([
['APT', undefined],
['a coin', LEGACY_USDC],
['a fungible asset', USDC_FA],
])('signs the serialized %s payload at the quoted gas', async (_, contractAddress) => {
const payload = getSerializedTransferPayload(ADDR, '1000', contractAddress)

vi.mocked(generateRawTransaction).mockImplementationOnce(args =>
Promise.resolve(
new RawTransaction(
AccountAddress.ONE,
7n,
args.payload as RawTransaction['payload'],
2000n,
100n,
99n,
new ChainId(1),
),
),
)

const { rawTransactionBytes } = await adapter.buildCustomApiTx({
from: SENDER,
accountNumber: 0,
payload,
gasUnitPrice: '150',
maxGasAmount: '4639',
})

const raw = RawTransaction.deserialize(new Deserializer(rawTransactionBytes))

expect(raw.max_gas_amount).toBe(4639n)
expect(raw.gas_unit_price).toBe(150n)
expect(Hex.fromHexInput(raw.payload.bcsToBytes()).toString()).toBe(payload)
})
})

describe('getFeeData', () => {
const getFeeData = () =>
adapter.getFeeData({
Expand Down Expand Up @@ -296,7 +343,7 @@ describe('AptosChainAdapter', () => {
})
})

it('simulates apt sends with 1 octa so a near full balance amount still gets fees', async () => {
it('simulates the given value so a sender short of it fails', async () => {
client.transaction.simulate.simple.mockResolvedValueOnce([simulation])

await adapter.getFeeData({
Expand All @@ -307,10 +354,18 @@ describe('AptosChainAdapter', () => {

expect(client.transaction.build.simple.mock.calls[0][0].data.functionArguments).toEqual([
ADDR,
1n,
198633150n,
])
})

it('simulates with a fee payer so a sender without apt still gets fees', async () => {
client.transaction.simulate.simple.mockResolvedValueOnce([simulation])

await getFeeData()

expect(client.transaction.build.simple.mock.calls[0][0].withFeePayer).toBe(true)
})

it.each([
['fails', { success: false, gas_used: '0', vm_status: 'INSUFFICIENT_BALANCE' }],
['has no fee statement', { success: true, events: [] }],
Expand Down
90 changes: 62 additions & 28 deletions packages/chain-adapters/src/aptos/AptosChainAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Deserializer,
Ed25519PublicKey,
Ed25519Signature,
generateRawTransaction,
Hex,
isPendingTransactionResponse,
isUserTransactionResponse,
Expand All @@ -15,6 +16,7 @@ import {
RawTransaction,
SignedTransaction,
TransactionAuthenticatorEd25519,
TransactionPayload,
} from '@aptos-labs/ts-sdk'
import type { AssetId, ChainId } from '@shapeshiftoss/caip'
import {
Expand Down Expand Up @@ -48,9 +50,14 @@ import type {
} from '../types'
import { ChainAdapterDisplayName, ValidAddressResultType } from '../types'
import { toAddressNList, verifyLedgerAppOpen } from '../utils'
import { APT_COIN_TYPE, FEE_STATEMENT_EVENT_TYPE, INDEXER_PAGE_LIMIT } from './constants'
import {
APT_COIN_TYPE,
FEE_STATEMENT_EVENT_TYPE,
INDEXER_PAGE_LIMIT,
TX_EXPIRY_SECS,
} from './constants'
import { parseAptosTx } from './parser'
import type { Token } from './types'
import type { BuildCustomApiTxInput, Token } from './types'
import { getBalancePayload, getDecimalsPayload, getTransferPayload } from './utils'

export interface ChainAdapterArgs {
Expand All @@ -77,6 +84,7 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.AptosMainnet> {
fullnode: `${args.rpcUrl}/v1`,
indexer: `${args.indexerUrl}/v1/graphql`,
clientConfig: args.apiKey ? { API_KEY: args.apiKey } : undefined,
transactionGenerationConfig: { defaultTxnExpirySecFromNow: TX_EXPIRY_SECS },
}),
)
}
Expand Down Expand Up @@ -272,31 +280,62 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.AptosMainnet> {
data: getTransferPayload(to, value, contractAddress),
})

// the sdk floors max gas at 2000 units, which would reserve far more apt than the fee
const { sender, sequence_number, payload, expiration_timestamp_secs, chain_id } =
rawTransaction

const transaction = new RawTransaction(
sender,
sequence_number,
payload,
BigInt(maxGasAmount),
BigInt(gasUnitPrice),
expiration_timestamp_secs,
chain_id,
)
return this.toSignTx(rawTransaction, { accountNumber, gasUnitPrice, maxGasAmount })
} catch (err) {
return ErrorHandler(err, {
translation: 'chainAdapters.errors.buildTransaction',
})
}
}

return {
addressNList: toAddressNList(this.getBip44Params({ accountNumber })),
rawTransactionBytes: transaction.bcsToBytes(),
}
async buildCustomApiTx(
input: BuildCustomApiTxInput,
): Promise<SignTx<KnownChainIds.AptosMainnet>> {
try {
const { from, accountNumber, payload, gasUnitPrice, maxGasAmount } = input

const rawTransaction = await generateRawTransaction({
aptosConfig: this.client.config,
sender: from,
payload: TransactionPayload.deserialize(
new Deserializer(Hex.fromHexInput(payload).toUint8Array()),
),
})

return this.toSignTx(rawTransaction, { accountNumber, gasUnitPrice, maxGasAmount })
} catch (err) {
return ErrorHandler(err, {
translation: 'chainAdapters.errors.buildTransaction',
})
}
}

private toSignTx(
rawTransaction: RawTransaction,
{
accountNumber,
gasUnitPrice,
maxGasAmount,
}: Pick<BuildCustomApiTxInput, 'accountNumber' | 'gasUnitPrice' | 'maxGasAmount'>,
): SignTx<KnownChainIds.AptosMainnet> {
const { sender, sequence_number, payload, expiration_timestamp_secs, chain_id } = rawTransaction

const transaction = new RawTransaction(
sender,
sequence_number,
payload,
BigInt(maxGasAmount),
BigInt(gasUnitPrice),
expiration_timestamp_secs,
chain_id,
)

return {
addressNList: toAddressNList(this.getBip44Params({ accountNumber })),
rawTransactionBytes: transaction.bcsToBytes(),
}
}

async buildSendTransaction(input: BuildSendTxInput<KnownChainIds.AptosMainnet>): Promise<{
txToSign: SignTx<KnownChainIds.AptosMainnet>
}> {
Expand Down Expand Up @@ -388,23 +427,18 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.AptosMainnet> {
const { to, value, chainSpecific } = input
const { from, contractAddress } = chainSpecific

// gas doesn't depend on the amount, and an apt amount near the balance can't cover gas in simulation
const simulatedValue = contractAddress ? value : '1'

const [{ gas_estimate, prioritized_gas_estimate, deprioritized_gas_estimate }, transaction] =
await Promise.all([
this.client.getGasPriceEstimation(),
this.client.transaction.build.simple({
sender: from,
data: getTransferPayload(to, simulatedValue, contractAddress),
data: getTransferPayload(to, value, contractAddress),
// sets the zero address as fee payer, which simulation accepts without checking anyone's gas balance
withFeePayer: true,
}),
])

// caps simulated gas at what the sender can afford, so the default cap can't fail the simulation
const [sim] = await this.client.transaction.simulate.simple({
transaction,
options: { estimateGasUnitPrice: true, estimateMaxGasAmount: true },
})
const [sim] = await this.client.transaction.simulate.simple({ transaction })

if (!sim?.success) throw new Error(`transaction simulation failed: ${sim?.vm_status}`)

Expand Down
3 changes: 3 additions & 0 deletions packages/chain-adapters/src/aptos/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export const FEE_STATEMENT_EVENT_TYPE = '0x1::transaction_fee::FeeStatement'

export const INDEXER_PAGE_LIMIT = 100

// the sdk default of 20s starts at build, so it can run out while a slow signer is confirming
export const TX_EXPIRY_SECS = 300

export const COIN_TRANSFER_FUNCTIONS = [
'0x1::aptos_account::transfer',
'0x1::aptos_account::transfer_coins',
Expand Down
6 changes: 6 additions & 0 deletions packages/chain-adapters/src/aptos/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export type BuildTxInput = FeeData & {
contractAddress: string | undefined
}

export type BuildCustomApiTxInput = FeeData & {
from: string
accountNumber: number
payload: string
}

export type GetFeeDataInput = {
from: string
contractAddress: string | undefined
Expand Down
16 changes: 13 additions & 3 deletions packages/chain-adapters/src/aptos/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { InputEntryFunctionData, InputViewFunctionData } from '@aptos-labs/ts-sdk'
import { AccountAddress } from '@aptos-labs/ts-sdk'
import type { InputEntryFunctionDataWithABI, InputViewFunctionData } from '@aptos-labs/ts-sdk'
import { AccountAddress, generateTransactionPayloadWithABI, Hex } from '@aptos-labs/ts-sdk'
import { ASSET_NAMESPACE, getAptosAssetNamespace } from '@shapeshiftoss/caip'

import { APT_COIN_TYPE, COIN_TRANSFER_ABI, FA_TRANSFER_ABI } from './constants'
Expand All @@ -20,7 +20,7 @@ export const getTransferPayload = (
to: string,
value: string,
contractAddress: string | undefined,
): InputEntryFunctionData => {
): InputEntryFunctionDataWithABI => {
if (contractAddress && isFungibleAssetReference(contractAddress)) {
return {
function: '0x1::primary_fungible_store::transfer',
Expand All @@ -38,6 +38,16 @@ export const getTransferPayload = (
}
}

export const getSerializedTransferPayload = (
to: string,
value: string,
contractAddress: string | undefined,
): string => {
const payload = generateTransactionPayloadWithABI(getTransferPayload(to, value, contractAddress))

return Hex.fromHexInput(payload.bcsToBytes()).toString()
}

export const getDecimalsPayload = (assetType: string): InputViewFunctionData => {
if (isFungibleAssetReference(assetType)) {
return {
Expand Down
2 changes: 2 additions & 0 deletions packages/public-api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ NEAR_NODE_URL=https://rpc.fastnear.com
NEAR_NODE_URL_FALLBACK_1=https://near.drpc.org
NEAR_NODE_URL_FALLBACK_2=https://rpc.mainnet.near.org
FASTNEAR_API_URL=https://api.fastnear.com
APTOS_NODE_URL=https://fullnode.mainnet.aptoslabs.com
APTOS_INDEXER_URL=https://api.mainnet.aptoslabs.com

# Midgard URLs
THORCHAIN_MIDGARD_URL=https://dev-api.thorchain.shapeshift.com/midgard/v2
Expand Down
2 changes: 2 additions & 0 deletions packages/public-api/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ const envSchema = z.object({
NEAR_NODE_URL_FALLBACK_1: z.union([url, z.literal('')]).default(''),
NEAR_NODE_URL_FALLBACK_2: z.union([url, z.literal('')]).default(''),
FASTNEAR_API_URL: url,
APTOS_NODE_URL: url,
APTOS_INDEXER_URL: url,

// Midgard URLs
THORCHAIN_MIDGARD_URL: url,
Expand Down
7 changes: 7 additions & 0 deletions packages/public-api/src/swapperDeps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,13 +282,19 @@ const starknetAdapter = new adapters.starknet.ChainAdapter({
),
})

const aptosAdapter = new adapters.aptos.ChainAdapter({
rpcUrl: env.APTOS_NODE_URL,
indexerUrl: env.APTOS_INDEXER_URL,
})

const otherAdaptersById: Record<ChainId, unknown> = {
[KnownChainIds.SolanaMainnet]: solanaAdapter,
[KnownChainIds.TronMainnet]: tronAdapter,
[KnownChainIds.SuiMainnet]: suiAdapter,
[KnownChainIds.TonMainnet]: tonAdapter,
[KnownChainIds.NearMainnet]: nearAdapter,
[KnownChainIds.StarknetMainnet]: starknetAdapter,
[KnownChainIds.AptosMainnet]: aptosAdapter,
}

export const getSwapperDeps = (): SwapperDeps => ({
Expand Down Expand Up @@ -325,5 +331,6 @@ export const getSwapperDeps = (): SwapperDeps => ({
assertGetNearChainAdapter: () => nearAdapter,
assertGetStarknetChainAdapter: () => starknetAdapter,
assertGetTonChainAdapter: () => tonAdapter,
assertGetAptosChainAdapter: () => aptosAdapter,
fetchIsSmartContractAddressQuery: () => Promise.resolve(false),
})
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Swapper } from '../../types'
import {
executeAptosTransaction,
executeEvmTransaction,
executeNearTransaction,
executeSolanaTransaction,
Expand All @@ -18,6 +19,7 @@ export const nearIntentsSwapper: Swapper = {
executeSuiTransaction,
executeNearTransaction,
executeTonTransaction,
executeAptosTransaction,
executeUtxoTransaction: (txToSign, { signAndBroadcastTransaction }) => {
return signAndBroadcastTransaction(txToSign)
},
Expand Down
Loading
Loading