Skip to content

Commit e013ce3

Browse files
Discostuswdiscordia
authored andcommitted
feat: add PanoraSwapper implementation (web-ohh.11)
1 parent a6b12c9 commit e013ce3

8 files changed

Lines changed: 190 additions & 24 deletions

File tree

packages/chain-adapters/src/aptos/AptosChainAdapter.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -234,18 +234,14 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.AptosMainnet> {
234234
return recommended > MIN_MAX_GAS_AMOUNT ? recommended : MIN_MAX_GAS_AMOUNT
235235
}
236236

237-
async buildSendApiTransaction(
238-
input: BuildSendApiTxInput<KnownChainIds.AptosMainnet>,
239-
): Promise<SignTx<KnownChainIds.AptosMainnet>> {
237+
async buildEntryFunctionApiTransaction(input: {
238+
from: string
239+
accountNumber: number
240+
data: InputEntryFunctionData
241+
}): Promise<SignTx<KnownChainIds.AptosMainnet>> {
240242
try {
241-
const { from, accountNumber, to, value, chainSpecific } = input
242-
const coinType = chainSpecific?.coinType ?? APT_COIN_TYPE
243+
const { from, accountNumber, data } = input
243244

244-
const data: InputEntryFunctionData = {
245-
function: '0x1::aptos_account::transfer_coins',
246-
typeArguments: [coinType],
247-
functionArguments: [to, BigInt(value)],
248-
}
249245
const maxGasAmount = Number(await this.estimateMaxGasAmount(from, data))
250246

251247
const transaction = await this.client.transaction.build.simple({
@@ -269,6 +265,21 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.AptosMainnet> {
269265
}
270266
}
271267

268+
buildSendApiTransaction(
269+
input: BuildSendApiTxInput<KnownChainIds.AptosMainnet>,
270+
): Promise<SignTx<KnownChainIds.AptosMainnet>> {
271+
const { from, accountNumber, to, value, chainSpecific } = input
272+
const coinType = chainSpecific?.coinType ?? APT_COIN_TYPE
273+
274+
const data: InputEntryFunctionData = {
275+
function: '0x1::aptos_account::transfer_coins',
276+
typeArguments: [coinType],
277+
functionArguments: [to, BigInt(value)],
278+
}
279+
280+
return this.buildEntryFunctionApiTransaction({ from, accountNumber, data })
281+
}
282+
272283
async buildSendTransaction(input: BuildSendTxInput<KnownChainIds.AptosMainnet>): Promise<{
273284
txToSign: SignTx<KnownChainIds.AptosMainnet>
274285
}> {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { executeAptosTransaction } from '../..'
2+
import type { Swapper } from '../../types'
3+
4+
export const panoraSwapper: Swapper = {
5+
executeAptosTransaction,
6+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { TxStatus } from '@shapeshiftoss/unchained-client'
2+
import { bnOrZero } from '@shapeshiftoss/utils'
3+
import type { Result } from '@sniptt/monads'
4+
5+
import { getDefaultSlippageDecimalPercentageForSwapper } from '../../constants'
6+
import type {
7+
CommonTradeQuoteInput,
8+
GetTradeRateInput,
9+
GetUnsignedAptosTransactionArgs,
10+
SwapErrorRight,
11+
SwapperApi,
12+
SwapperDeps,
13+
TradeQuote,
14+
TradeRate,
15+
TradeStatus,
16+
} from '../../types'
17+
import { SwapperName } from '../../types'
18+
import { checkAptosSwapStatus, isExecutableTradeQuote } from '../../utils'
19+
import { getPanoraTradeData } from './swapperApi/getPanoraTradeData'
20+
import { getTradeQuote } from './swapperApi/getTradeQuote'
21+
import { getTradeRate } from './swapperApi/getTradeRate'
22+
23+
export const panoraApi: SwapperApi = {
24+
getTradeQuote: (
25+
input: CommonTradeQuoteInput,
26+
deps: SwapperDeps,
27+
): Promise<Result<TradeQuote[], SwapErrorRight>> => {
28+
return getTradeQuote(input, deps)
29+
},
30+
31+
getTradeRate: (
32+
input: GetTradeRateInput,
33+
deps: SwapperDeps,
34+
): Promise<Result<TradeRate[], SwapErrorRight>> => {
35+
return getTradeRate(input, deps)
36+
},
37+
38+
getUnsignedAptosTransaction: async ({
39+
stepIndex,
40+
tradeQuote,
41+
from,
42+
assertGetAptosChainAdapter,
43+
config,
44+
}: GetUnsignedAptosTransactionArgs) => {
45+
if (!isExecutableTradeQuote(tradeQuote)) throw new Error('Unable to execute a trade rate quote')
46+
47+
const step = tradeQuote.steps[stepIndex ?? 0]
48+
if (!step) throw new Error(`No step at index ${stepIndex}`)
49+
50+
const { accountNumber, sellAsset, buyAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit } =
51+
step
52+
53+
const adapter = assertGetAptosChainAdapter(sellAsset.chainId)
54+
55+
// Re-fetch quote from Panora to get fresh txData for execution
56+
const slippagePercentage = bnOrZero(
57+
tradeQuote.slippageTolerancePercentageDecimal ??
58+
getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Panora),
59+
)
60+
.times(100)
61+
.toNumber()
62+
63+
const tradeDataResult = await getPanoraTradeData({
64+
sellAsset,
65+
buyAsset,
66+
sellAmountIncludingProtocolFeesCryptoBaseUnit,
67+
receiveAddress: from,
68+
affiliateBps: tradeQuote.affiliateBps,
69+
slippagePercentage,
70+
config,
71+
})
72+
73+
if (tradeDataResult.isErr()) {
74+
throw new Error(`Failed to get Panora trade data: ${tradeDataResult.unwrapErr().message}`)
75+
}
76+
77+
const { txData } = tradeDataResult.unwrap()
78+
79+
return adapter.buildEntryFunctionApiTransaction({
80+
from,
81+
accountNumber: accountNumber ?? 0,
82+
data: {
83+
function: txData.function as `${string}::${string}::${string}`,
84+
typeArguments: txData.type_arguments,
85+
functionArguments: txData.arguments,
86+
},
87+
})
88+
},
89+
90+
getAptosTransactionFees: ({ tradeQuote, stepIndex }: GetUnsignedAptosTransactionArgs) => {
91+
if (!isExecutableTradeQuote(tradeQuote)) throw new Error('Unable to execute a trade rate quote')
92+
93+
const step = tradeQuote.steps[stepIndex ?? 0]
94+
if (!step) throw new Error('Missing step')
95+
if (!step.feeData.networkFeeCryptoBaseUnit) {
96+
throw new Error('Missing network fee in quote')
97+
}
98+
return Promise.resolve(step.feeData.networkFeeCryptoBaseUnit)
99+
},
100+
101+
checkTradeStatus: ({ swap, assertGetAptosChainAdapter }): Promise<TradeStatus> => {
102+
if (!swap?.sellTxHash) {
103+
return Promise.resolve({
104+
status: TxStatus.Unknown,
105+
buyTxHash: undefined,
106+
message: undefined,
107+
})
108+
}
109+
110+
return checkAptosSwapStatus({
111+
txHash: swap.sellTxHash,
112+
address: swap.receiveAddress,
113+
assertGetAptosChainAdapter,
114+
})
115+
},
116+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export {
2+
PANORA_NATIVE_TOKEN_ADDRESS,
3+
PANORA_DEFAULT_SLIPPAGE_DECIMAL_PERCENTAGE,
4+
SUPPORTED_PANORA_CHAIN_IDS,
5+
} from './utils/constants'

packages/swapper/src/swappers/PanoraSwapper/swapperApi/getPanoraTradeData.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { bnOrZero } from '@shapeshiftoss/utils'
1+
import type { AssetId } from '@shapeshiftoss/caip'
2+
import type { Asset } from '@shapeshiftoss/types'
3+
import { bnOrZero, fromBaseUnit, toBaseUnit } from '@shapeshiftoss/utils'
24
import type { Result } from '@sniptt/monads'
35
import { Err, Ok } from '@sniptt/monads'
46

5-
import type { AssetId } from '@shapeshiftoss/caip'
6-
import type { Asset } from '@shapeshiftoss/types'
77
import type { ProtocolFee, SwapErrorRight, SwapperConfig } from '../../../types'
88
import { TradeQuoteError } from '../../../types'
99
import { getInputOutputRate, makeSwapErrorRight } from '../../../utils'
10-
import { isSupportedChainId } from '../utils/constants'
10+
import type { PanoraSwapResponse } from '../types'
11+
import { isSupportedChainId, PANORA_INTEGRATOR_FEE_ADDRESS } from '../utils/constants'
1112
import { getTokenAddress } from '../utils/helpers'
1213
import { getPanoraService } from '../utils/panoraService'
13-
import type { PanoraSwapResponse } from '../types'
1414

1515
type PanoraTradeDataInput = {
1616
sellAsset: Asset
@@ -79,13 +79,24 @@ export const getPanoraTradeData = async (
7979
const requestBody: Record<string, unknown> = {
8080
fromTokenAddress,
8181
toTokenAddress,
82-
fromTokenAmount: sellAmountIncludingProtocolFeesCryptoBaseUnit,
82+
// The Panora API deals in human-readable token amounts, not base units
83+
fromTokenAmount: fromBaseUnit(
84+
sellAmountIncludingProtocolFeesCryptoBaseUnit,
85+
sellAsset.precision,
86+
),
8387
toWalletAddress: receiveAddress,
8488
slippagePercentage,
8589
}
8690

87-
if (integratorFeePercentage !== undefined && integratorFeePercentage > 0) {
91+
// Panora deducts integratorFeePercentage from the user even without integratorFeeAddress,
92+
// but only routes it on-chain when the address is present. Never send the percentage alone.
93+
if (
94+
PANORA_INTEGRATOR_FEE_ADDRESS &&
95+
integratorFeePercentage !== undefined &&
96+
integratorFeePercentage > 0
97+
) {
8898
requestBody.integratorFeePercentage = integratorFeePercentage
99+
requestBody.integratorFeeAddress = PANORA_INTEGRATOR_FEE_ADDRESS
89100
}
90101

91102
try {
@@ -110,7 +121,7 @@ export const getPanoraTradeData = async (
110121
// Take the best quote (first one)
111122
const bestQuote = data.quotes[0]
112123

113-
const buyAmountAfterFeesCryptoBaseUnit = bestQuote.toTokenAmount
124+
const buyAmountAfterFeesCryptoBaseUnit = toBaseUnit(bestQuote.toTokenAmount, buyAsset.precision)
114125

115126
const rate = getInputOutputRate({
116127
sellAmountCryptoBaseUnit: sellAmountIncludingProtocolFeesCryptoBaseUnit,

packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeQuote.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { CommonTradeQuoteInput, SwapErrorRight, SwapperDeps, TradeQuote } f
88
import { SwapperName, TradeQuoteError } from '../../../types'
99
import { makeSwapErrorRight } from '../../../utils'
1010
import { buildAffiliateFee } from '../../utils/affiliateFee'
11+
import { PANORA_INTEGRATOR_FEE_ADDRESS } from '../utils/constants'
1112
import { getPanoraTradeData } from './getPanoraTradeData'
1213

1314
export const getTradeQuote = async (
@@ -49,12 +50,16 @@ export const getTradeQuote = async (
4950
.times(100)
5051
.toNumber()
5152

53+
// No integrator fee address is configured yet, so Panora cannot route an affiliate fee
54+
// to the DAO; zero it out rather than displaying a fee that is not collected.
55+
const appliedAffiliateBps = PANORA_INTEGRATOR_FEE_ADDRESS ? affiliateBps : '0'
56+
5257
const tradeDataResult = await getPanoraTradeData({
5358
sellAsset,
5459
buyAsset,
5560
sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount,
5661
receiveAddress,
57-
affiliateBps,
62+
affiliateBps: appliedAffiliateBps,
5863
slippagePercentage,
5964
config: deps.config,
6065
})
@@ -71,7 +76,7 @@ export const getTradeQuote = async (
7176
id: uuid(),
7277
quoteOrRate: 'quote',
7378
rate,
74-
affiliateBps,
79+
affiliateBps: appliedAffiliateBps,
7580
receiveAddress,
7681
slippageTolerancePercentageDecimal,
7782
swapperName: SwapperName.Panora,
@@ -97,7 +102,7 @@ export const getTradeQuote = async (
97102
estimatedExecutionTimeMs: undefined,
98103
affiliateFee: buildAffiliateFee({
99104
strategy: 'buy_asset',
100-
affiliateBps,
105+
affiliateBps: appliedAffiliateBps,
101106
sellAsset,
102107
buyAsset,
103108
sellAmountCryptoBaseUnit: sellAmount,

packages/swapper/src/swappers/PanoraSwapper/swapperApi/getTradeRate.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { GetTradeRateInput, SwapErrorRight, SwapperDeps, TradeRate } from '
88
import { SwapperName, TradeQuoteError } from '../../../types'
99
import { makeSwapErrorRight } from '../../../utils'
1010
import { buildAffiliateFee } from '../../utils/affiliateFee'
11+
import { PANORA_INTEGRATOR_FEE_ADDRESS } from '../utils/constants'
1112
import { getPanoraTradeData } from './getPanoraTradeData'
1213

1314
const DUMMY_WALLET_ADDRESS = '0x0000000000000000000000000000000000000000000000000000000000000000'
@@ -34,12 +35,16 @@ export const getTradeRate = async (
3435

3536
const walletAddress = receiveAddress ?? DUMMY_WALLET_ADDRESS
3637

38+
// No integrator fee address is configured yet, so Panora cannot route an affiliate fee
39+
// to the DAO; zero it out rather than displaying a fee that is not collected.
40+
const appliedAffiliateBps = PANORA_INTEGRATOR_FEE_ADDRESS ? affiliateBps : '0'
41+
3742
const tradeDataResult = await getPanoraTradeData({
3843
sellAsset,
3944
buyAsset,
4045
sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmount,
4146
receiveAddress: walletAddress,
42-
affiliateBps,
47+
affiliateBps: appliedAffiliateBps,
4348
slippagePercentage,
4449
config: deps.config,
4550
})
@@ -55,7 +60,7 @@ export const getTradeRate = async (
5560
id: uuid(),
5661
quoteOrRate: 'rate',
5762
rate,
58-
affiliateBps,
63+
affiliateBps: appliedAffiliateBps,
5964
receiveAddress,
6065
slippageTolerancePercentageDecimal,
6166
swapperName: SwapperName.Panora,
@@ -77,7 +82,7 @@ export const getTradeRate = async (
7782
estimatedExecutionTimeMs: undefined,
7883
affiliateFee: buildAffiliateFee({
7984
strategy: 'buy_asset',
80-
affiliateBps,
85+
affiliateBps: appliedAffiliateBps,
8186
sellAsset,
8287
buyAsset,
8388
sellAmountCryptoBaseUnit: sellAmount,

packages/swapper/src/swappers/PanoraSwapper/utils/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,10 @@ export const isSupportedChainId = (
1111

1212
export const PANORA_NATIVE_TOKEN_ADDRESS = '0xa'
1313
export const PANORA_DEFAULT_SLIPPAGE_DECIMAL_PERCENTAGE = '0.005'
14+
15+
// Panora routes integrator fees on-chain only when integratorFeeAddress accompanies
16+
// integratorFeePercentage; the percentage alone is still deducted from the user but kept
17+
// by Panora (verified against the live /swap API). Leave unset until the DAO has an Aptos
18+
// treasury address (the DAO_TREASURY_APTOS placeholder was removed during review for the
19+
// same reason).
20+
export const PANORA_INTEGRATOR_FEE_ADDRESS: string | undefined = undefined

0 commit comments

Comments
 (0)