diff --git a/generated/smartContract/contract_xlink_btc-peg-in-endpoint-v2-07-swap.ts b/generated/smartContract/contract_xlink_btc-peg-in-endpoint-v2-07-swap.ts new file mode 100644 index 0000000..f480829 --- /dev/null +++ b/generated/smartContract/contract_xlink_btc-peg-in-endpoint-v2-07-swap.ts @@ -0,0 +1,243 @@ + +import { +defineContract, +principalT, +bufferT, +responseSimpleT, +booleanT, +tupleT, +uintT, +listT, +traitT, +optionalT, +stringAsciiT, +noneT +} from "../smartContractHelpers/codegenImport" + +export const btcPegInEndpointV207Swap = defineContract({ +"btc-peg-in-endpoint-v2-07-swap": { + callback: { + input: [ + { name: 'sender', type: principalT }, + { name: 'payload', type: bufferT } + ], + output: responseSimpleT(booleanT, ), + mode: 'public' + }, + 'finalize-peg-in-cross-swap': { + input: [ + { name: 'tx', type: bufferT }, + { + name: 'block', + type: tupleT({ header: bufferT, height: uintT }, ) + }, + { + name: 'proof', + type: tupleT({ hashes: listT(bufferT, ), 'tree-depth': uintT, 'tx-index': uintT }, ) + }, + { name: 'output-idx', type: uintT }, + { + name: 'reveal-tx', + type: tupleT({ 'order-idx': uintT, tx: bufferT }, ) + }, + { name: 'routing-traits', type: listT(traitT, ) }, + { name: 'token-out-trait', type: traitT } + ], + output: responseSimpleT(booleanT, ), + mode: 'public' + }, + 'pause-peg-in': { + input: [ { name: 'paused', type: booleanT } ], + output: responseSimpleT(booleanT, ), + mode: 'public' + }, + 'set-btc-peg-out-fee': { + input: [ { name: 'fee', type: uintT } ], + output: responseSimpleT(booleanT, ), + mode: 'public' + }, + 'set-btc-peg-out-min-fee': { + input: [ { name: 'fee', type: uintT } ], + output: responseSimpleT(booleanT, ), + mode: 'public' + }, + 'set-fee-to-address': { + input: [ { name: 'new-fee-to-address', type: principalT } ], + output: responseSimpleT(booleanT, ), + mode: 'public' + }, + 'set-peg-in-fee': { + input: [ { name: 'fee', type: uintT } ], + output: responseSimpleT(booleanT, ), + mode: 'public' + }, + 'set-peg-in-min-fee': { + input: [ { name: 'fee', type: uintT } ], + output: responseSimpleT(booleanT, ), + mode: 'public' + }, + 'break-routing-id': { + input: [ { name: 'routing-ids', type: listT(uintT, ) } ], + output: responseSimpleT(tupleT({ + 'routing-factors': listT(uintT, ), + 'routing-tokens': listT(principalT, ) + }, ), ), + mode: 'readonly' + }, + 'construct-principal': { + input: [ { name: 'hash-bytes', type: bufferT } ], + output: responseSimpleT(principalT, ), + mode: 'readonly' + }, + 'create-order-cross-swap-or-fail': { + input: [ + { + name: 'order', + type: tupleT({ + 'chain-id': optionalT(uintT, ), + from: bufferT, + 'min-amount-out': optionalT(uintT, ), + routing: listT(uintT, ), + to: bufferT, + 'token-out': principalT + }, ) + } + ], + output: responseSimpleT(bufferT, ), + mode: 'readonly' + }, + 'decode-from-reveal-tx-or-fail': { + input: [ + { name: 'tx', type: bufferT }, + { name: 'order-idx', type: uintT } + ], + output: responseSimpleT(tupleT({ 'commit-txid': bufferT, 'order-script': bufferT }, ), ), + mode: 'readonly' + }, + 'decode-order-cross-swap-from-reveal-tx-or-fail': { + input: [ + { name: 'tx', type: bufferT }, + { name: 'order-idx', type: uintT } + ], + output: responseSimpleT(tupleT({ + 'commit-txid': bufferT, + 'order-details': tupleT({ + 'chain-id': optionalT(uintT, ), + from: bufferT, + 'min-amount-out': optionalT(uintT, ), + routing: listT(uintT, ), + to: bufferT, + 'token-out': principalT + }, ) + }, ), ), + mode: 'readonly' + }, + 'decode-order-cross-swap-or-fail': { + input: [ { name: 'order-script', type: bufferT } ], + output: responseSimpleT(tupleT({ + 'chain-id': optionalT(uintT, ), + from: bufferT, + 'min-amount-out': optionalT(uintT, ), + routing: listT(uintT, ), + to: bufferT, + 'token-out': principalT + }, ), ), + mode: 'readonly' + }, + 'destruct-principal': { + input: [ { name: 'address', type: principalT } ], + output: responseSimpleT(tupleT({ + 'hash-bytes': bufferT, + name: optionalT(stringAsciiT, ), + version: bufferT + }, ), ), + mode: 'readonly' + }, + 'extract-tx-ins-outs': { + input: [ { name: 'tx', type: bufferT } ], + output: responseSimpleT(tupleT({ + ins: listT(tupleT({ + outpoint: tupleT({ hash: bufferT, index: uintT }, ), + scriptSig: bufferT, + sequence: uintT + }, ), ), + outs: listT(tupleT({ scriptPubKey: bufferT, value: uintT }, ), ) + }, ), ), + mode: 'readonly' + }, + 'get-btc-peg-out-fee': { input: [], output: uintT, mode: 'readonly' }, + 'get-btc-peg-out-min-fee': { input: [], output: uintT, mode: 'readonly' }, + 'get-fee-to-address': { input: [], output: principalT, mode: 'readonly' }, + 'get-peg-in-fee': { input: [], output: uintT, mode: 'readonly' }, + 'get-peg-in-min-fee': { input: [], output: uintT, mode: 'readonly' }, + 'get-peg-in-sent-or-default': { + input: [ { name: 'tx', type: bufferT }, { name: 'output', type: uintT } ], + output: booleanT, + mode: 'readonly' + }, + 'get-txid': { + input: [ { name: 'tx', type: bufferT } ], + output: responseSimpleT(bufferT, ), + mode: 'readonly' + }, + 'is-dao-or-extension': { input: [], output: responseSimpleT(booleanT, ), mode: 'readonly' }, + 'is-peg-in-address-approved': { + input: [ { name: 'address', type: bufferT } ], + output: booleanT, + mode: 'readonly' + }, + 'is-peg-in-paused': { input: [], output: booleanT, mode: 'readonly' }, + 'validate-tx-cross-swap': { + input: [ + { + name: 'commit-tx', + type: tupleT({ 'output-idx': uintT, tx: bufferT }, ) + }, + { + name: 'reveal-tx', + type: tupleT({ 'order-idx': uintT, tx: bufferT }, ) + }, + { name: 'routing-traits', type: listT(traitT, ) }, + { name: 'token-out-trait', type: traitT } + ], + output: responseSimpleT(tupleT({ + 'amount-net': uintT, + fee: uintT, + 'order-details': tupleT({ + 'chain-id': optionalT(uintT, ), + from: bufferT, + 'min-amount-out': optionalT(uintT, ), + routing: listT(uintT, ), + to: bufferT, + 'token-out': principalT + }, ), + 'routing-factors': listT(uintT, ), + 'routing-tokens': listT(principalT, ) + }, ), ), + mode: 'readonly' + }, + 'verify-mined': { + input: [ + { name: 'tx', type: bufferT }, + { + name: 'block', + type: tupleT({ header: bufferT, height: uintT }, ) + }, + { + name: 'proof', + type: tupleT({ hashes: listT(bufferT, ), 'tree-depth': uintT, 'tx-index': uintT }, ) + } + ], + output: responseSimpleT(booleanT, ), + mode: 'readonly' + }, + 'btc-peg-out-min-fee': { input: noneT, output: uintT, mode: 'variable' }, + 'btc-peg-outfee': { input: noneT, output: uintT, mode: 'variable' }, + 'fee-to-address': { input: noneT, output: principalT, mode: 'variable' }, + 'peg-in-fee': { input: noneT, output: uintT, mode: 'variable' }, + 'peg-in-min-fee': { input: noneT, output: uintT, mode: 'variable' }, + 'peg-in-paused': { input: noneT, output: booleanT, mode: 'variable' } +} +} as const) + + diff --git a/generated/smartContract/contract_xlink_meta-peg-in-endpoint-v2-06-swap.ts b/generated/smartContract/contract_xlink_meta-peg-in-endpoint-v2-06-swap.ts new file mode 100644 index 0000000..5ee3e7f --- /dev/null +++ b/generated/smartContract/contract_xlink_meta-peg-in-endpoint-v2-06-swap.ts @@ -0,0 +1,330 @@ + +import { +defineContract, +tupleT, +optionalT, +uintT, +bufferT, +listT, +traitT, +responseSimpleT, +booleanT, +stringT, +principalT, +noneT +} from "../smartContractHelpers/codegenImport" + +export const metaPegInEndpointV206Swap = defineContract({ +"meta-peg-in-endpoint-v2-06-swap": { + 'finalize-peg-in-cross-swap': { + input: [ + { + name: 'commit-tx', + type: tupleT({ 'fee-idx': optionalT(uintT, ), 'output-idx': uintT, tx: bufferT }, ) + }, + { + name: 'reveal-tx', + type: tupleT({ 'order-idx': uintT, tx: bufferT }, ) + }, + { + name: 'reveal-block', + type: tupleT({ header: bufferT, height: uintT }, ) + }, + { + name: 'reveal-proof', + type: tupleT({ hashes: listT(bufferT, ), 'tree-depth': uintT, 'tx-index': uintT }, ) + }, + { name: 'routing-traits', type: listT(traitT, ) }, + { name: 'token-out-trait', type: traitT } + ], + output: responseSimpleT(booleanT, ), + mode: 'public' + }, + 'finalize-peg-in-cross-swap-on-index': { + input: [ + { + name: 'tx', + type: tupleT({ + amt: uintT, + 'bitcoin-tx': bufferT, + decimals: uintT, + from: bufferT, + 'from-bal': uintT, + output: uintT, + tick: stringT, + to: bufferT, + 'to-bal': uintT + }, ) + }, + { + name: 'block', + type: tupleT({ header: bufferT, height: uintT }, ) + }, + { + name: 'proof', + type: tupleT({ hashes: listT(bufferT, ), 'tree-depth': uintT, 'tx-index': uintT }, ) + }, + { + name: 'signature-packs', + type: listT(tupleT({ signature: bufferT, signer: principalT, 'tx-hash': bufferT }, ), ) + }, + { + name: 'reveal-tx', + type: tupleT({ 'order-idx': uintT, tx: bufferT }, ) + }, + { + name: 'reveal-block', + type: tupleT({ header: bufferT, height: uintT }, ) + }, + { + name: 'reveal-proof', + type: tupleT({ hashes: listT(bufferT, ), 'tree-depth': uintT, 'tx-index': uintT }, ) + }, + { name: 'fee-idx', type: optionalT(uintT, ) }, + { name: 'routing-traits', type: listT(traitT, ) }, + { name: 'token-out-trait', type: traitT } + ], + output: responseSimpleT(booleanT, ), + mode: 'public' + }, + pause: { + input: [ { name: 'new-paused', type: booleanT } ], + output: responseSimpleT(booleanT, ), + mode: 'public' + }, + 'set-btc-peg-out-fee': { + input: [ { name: 'fee', type: uintT } ], + output: responseSimpleT(booleanT, ), + mode: 'public' + }, + 'set-btc-peg-out-min-fee': { + input: [ { name: 'fee', type: uintT } ], + output: responseSimpleT(booleanT, ), + mode: 'public' + }, + 'set-fee-to-address': { + input: [ { name: 'new-fee-to-address', type: principalT } ], + output: responseSimpleT(booleanT, ), + mode: 'public' + }, + 'set-peg-in-fee': { + input: [ { name: 'fee', type: uintT } ], + output: responseSimpleT(booleanT, ), + mode: 'public' + }, + 'transfer-all-to': { + input: [ + { name: 'new-owner', type: principalT }, + { name: 'token-trait', type: traitT } + ], + output: responseSimpleT(booleanT, ), + mode: 'public' + }, + 'transfer-all-to-many': { + input: [ + { name: 'new-owner', type: principalT }, + { name: 'token-traits', type: listT(traitT, ) } + ], + output: responseSimpleT(listT(responseSimpleT(booleanT, ), ), ), + mode: 'public' + }, + 'break-routing-id': { + input: [ + { name: 'token-in', type: principalT }, + { name: 'routing-ids', type: listT(uintT, ) } + ], + output: responseSimpleT(tupleT({ + 'routing-factors': listT(uintT, ), + 'routing-tokens': listT(principalT, ) + }, ), ), + mode: 'readonly' + }, + 'create-order-cross-swap-or-fail': { + input: [ + { + name: 'order', + type: tupleT({ + 'chain-id': optionalT(uintT, ), + from: bufferT, + 'min-amount-out': optionalT(uintT, ), + routing: listT(uintT, ), + to: bufferT, + 'token-out': principalT + }, ) + } + ], + output: responseSimpleT(bufferT, ), + mode: 'readonly' + }, + 'decode-order-cross-swap-from-reveal-tx-or-fail': { + input: [ + { name: 'tx', type: bufferT }, + { name: 'order-idx', type: uintT } + ], + output: responseSimpleT(tupleT({ + 'commit-txid': bufferT, + 'order-details': tupleT({ + 'chain-id': optionalT(uintT, ), + from: bufferT, + 'min-amount-out': optionalT(uintT, ), + routing: listT(uintT, ), + to: bufferT, + 'token-out': principalT + }, ) + }, ), ), + mode: 'readonly' + }, + 'decode-order-cross-swap-or-fail': { + input: [ { name: 'order-script', type: bufferT } ], + output: responseSimpleT(tupleT({ + 'chain-id': optionalT(uintT, ), + from: bufferT, + 'min-amount-out': optionalT(uintT, ), + routing: listT(uintT, ), + to: bufferT, + 'token-out': principalT + }, ), ), + mode: 'readonly' + }, + 'get-btc-peg-out-fee': { input: [], output: uintT, mode: 'readonly' }, + 'get-btc-peg-out-min-fee': { input: [], output: uintT, mode: 'readonly' }, + 'get-fee-to-address': { input: [], output: principalT, mode: 'readonly' }, + 'get-pair-details': { + input: [ + { + name: 'pair', + type: tupleT({ 'chain-id': uintT, token: principalT }, ) + } + ], + output: optionalT(tupleT({ + approved: booleanT, + 'no-burn': booleanT, + 'peg-in-fee': uintT, + 'peg-in-paused': booleanT, + 'peg-out-fee': uintT, + 'peg-out-gas-fee': uintT, + 'peg-out-paused': booleanT, + tick: stringT + }, ), ), + mode: 'readonly' + }, + 'get-pair-details-many': { + input: [ + { + name: 'pairs', + type: listT(tupleT({ 'chain-id': uintT, token: principalT }, ), ) + } + ], + output: listT(optionalT(tupleT({ + approved: booleanT, + 'no-burn': booleanT, + 'peg-in-fee': uintT, + 'peg-in-paused': booleanT, + 'peg-out-fee': uintT, + 'peg-out-gas-fee': uintT, + 'peg-out-paused': booleanT, + tick: stringT + }, ), ), ), + mode: 'readonly' + }, + 'get-pair-details-or-fail': { + input: [ + { + name: 'pair', + type: tupleT({ 'chain-id': uintT, token: principalT }, ) + } + ], + output: responseSimpleT(tupleT({ + approved: booleanT, + 'no-burn': booleanT, + 'peg-in-fee': uintT, + 'peg-in-paused': booleanT, + 'peg-out-fee': uintT, + 'peg-out-gas-fee': uintT, + 'peg-out-paused': booleanT, + tick: stringT + }, ), ), + mode: 'readonly' + }, + 'get-peg-in-fee': { input: [], output: uintT, mode: 'readonly' }, + 'get-peg-in-sent-or-default': { + input: [ + { name: 'bitcoin-tx', type: bufferT }, + { name: 'output', type: uintT }, + { name: 'offset', type: uintT } + ], + output: booleanT, + mode: 'readonly' + }, + 'get-tick-to-pair-or-fail': { + input: [ { name: 'tick', type: stringT } ], + output: responseSimpleT(tupleT({ 'chain-id': uintT, token: principalT }, ), ), + mode: 'readonly' + }, + 'is-approved-pair': { + input: [ + { + name: 'pair', + type: tupleT({ 'chain-id': uintT, token: principalT }, ) + } + ], + output: booleanT, + mode: 'readonly' + }, + 'is-dao-or-extension': { input: [], output: responseSimpleT(booleanT, ), mode: 'readonly' }, + 'is-paused': { input: [], output: booleanT, mode: 'readonly' }, + 'is-peg-in-address-approved': { + input: [ { name: 'address', type: bufferT } ], + output: booleanT, + mode: 'readonly' + }, + 'validate-tx-cross-swap': { + input: [ + { + name: 'commit-tx', + type: tupleT({ 'fee-idx': optionalT(uintT, ), 'output-idx': uintT, tx: bufferT }, ) + }, + { + name: 'reveal-tx', + type: tupleT({ 'order-idx': uintT, tx: bufferT }, ) + }, + { name: 'routing-traits', type: listT(traitT, ) }, + { name: 'token-out-trait', type: traitT } + ], + output: responseSimpleT(tupleT({ + 'amt-net': uintT, + fee: uintT, + 'order-details': tupleT({ + 'chain-id': optionalT(uintT, ), + from: bufferT, + 'min-amount-out': optionalT(uintT, ), + routing: listT(uintT, ), + to: bufferT, + 'token-out': principalT + }, ), + 'pair-details': tupleT({ 'chain-id': uintT, token: principalT }, ), + 'routing-factors': listT(uintT, ), + 'routing-tokens': listT(principalT, ), + 'token-details': tupleT({ + approved: booleanT, + 'no-burn': booleanT, + 'peg-in-fee': uintT, + 'peg-in-paused': booleanT, + 'peg-out-fee': uintT, + 'peg-out-gas-fee': uintT, + 'peg-out-paused': booleanT, + tick: stringT + }, ), + 'tx-idxed': tupleT({ amt: uintT, from: bufferT, tick: stringT, to: bufferT }, ) + }, ), ), + mode: 'readonly' + }, + 'btc-peg-out-fee': { input: noneT, output: uintT, mode: 'variable' }, + 'btc-peg-out-min-fee': { input: noneT, output: uintT, mode: 'variable' }, + 'fee-to-address': { input: noneT, output: principalT, mode: 'variable' }, + paused: { input: noneT, output: booleanT, mode: 'variable' }, + 'peg-in-fee': { input: noneT, output: uintT, mode: 'variable' } +} +} as const) + + diff --git a/generated/smartContract/contracts_xlink.ts b/generated/smartContract/contracts_xlink.ts index 63c1d98..d9aea49 100644 --- a/generated/smartContract/contracts_xlink.ts +++ b/generated/smartContract/contracts_xlink.ts @@ -1,17 +1,21 @@ import { defineContract } from "../smartContractHelpers/codegenImport"; import { btcPegInEndpointV205 } from "./contract_xlink_btc-peg-in-endpoint-v2-05" +import { btcPegInEndpointV207Swap } from "./contract_xlink_btc-peg-in-endpoint-v2-07-swap" import { btcPegOutEndpointV201 } from "./contract_xlink_btc-peg-out-endpoint-v2-01" import { crossPegInEndpointV204 } from "./contract_xlink_cross-peg-in-endpoint-v2-04" import { crossPegOutEndpointV201 } from "./contract_xlink_cross-peg-out-endpoint-v2-01" import { metaPegInEndpointV204 } from "./contract_xlink_meta-peg-in-endpoint-v2-04" +import { metaPegInEndpointV206Swap } from "./contract_xlink_meta-peg-in-endpoint-v2-06-swap" import { metaPegOutEndpointV204 } from "./contract_xlink_meta-peg-out-endpoint-v2-04" export const xlinkContracts = defineContract({ ...btcPegInEndpointV205, +...btcPegInEndpointV207Swap, ...btcPegOutEndpointV201, ...crossPegInEndpointV204, ...crossPegOutEndpointV201, ...metaPegInEndpointV204, +...metaPegInEndpointV206Swap, ...metaPegOutEndpointV204 }); diff --git a/package.json b/package.json index 95c4c6e..3273263 100644 --- a/package.json +++ b/package.json @@ -41,9 +41,9 @@ "gen": "pnpm run gen:stacksContract", "docs": "typedoc", "docs:watch": "typedoc --watch", - "build": "pnpm run gen && rm -rf lib && tsup-node --sourcemap --dts -d lib --format cjs,esm src", + "build": "pnpm run gen && rm -rf lib && tsup-node src", "prepare": "pnpm run build", - "test": "vitest --exclude lib" + "test": "vitest --exclude lib --run" }, "dependencies": { "@c4/btc-utils": "^0.3.1", diff --git a/scripts/generateClarityTranscoders.ts b/scripts/generateClarityTranscoders.ts index 4b6ae3d..cf4e580 100644 --- a/scripts/generateClarityTranscoders.ts +++ b/scripts/generateClarityTranscoders.ts @@ -1,32 +1,52 @@ import { generateContracts } from "clarity-codegen/lib/generate" import * as path from "node:path" -import { STACKS_MAINNET } from "../src/config" import { - stxContractDeployers, + contractNameOverrides, + envName, + STACKS_MAINNET, + STACKS_TESTNET, +} from "../src/config" +import { + StacksContractName, + stxContractAddresses, xlinkContractsMultisigMainnet, + xlinkContractsMultisigTestnet, } from "../src/stacksUtils/stxContractAddresses" import { KnownChainId } from "../src/utils/types/knownIds" ;(async function main(): Promise { + const stacksChainId = + envName === "prod" + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet + const fallbackDeployerAddress = + envName === "prod" + ? xlinkContractsMultisigMainnet + : xlinkContractsMultisigTestnet + const fallbackStacksNetwork = + envName === "prod" ? STACKS_MAINNET : STACKS_TESTNET + await generateContracts( - process.env.STACKS_CORE_API_URL ?? STACKS_MAINNET.coreApiUrl, + process.env.STACKS_CORE_API_URL ?? fallbackStacksNetwork.coreApiUrl, contractName => { return ( - stxContractDeployers[ - contractName as keyof typeof stxContractDeployers - ]?.[KnownChainId.Stacks.Mainnet]?.deployerAddress ?? - xlinkContractsMultisigMainnet + stxContractAddresses[contractName as StacksContractName]?.[ + stacksChainId + ]?.deployerAddress ?? fallbackDeployerAddress ) }, [ "btc-peg-in-endpoint-v2-05", + "btc-peg-in-endpoint-v2-07-swap", "btc-peg-out-endpoint-v2-01", "cross-peg-in-endpoint-v2-04", "cross-peg-out-endpoint-v2-01", "meta-peg-in-endpoint-v2-04", + "meta-peg-in-endpoint-v2-06-swap", "meta-peg-out-endpoint-v2-04", ], path.resolve(__dirname, "../generated/smartContract/"), "xlink", "../smartContractHelpers/codegenImport", + contractNameOverrides, ) })().catch(console.error) diff --git a/scripts/printOnChainConfigs.ts b/scripts/printOnChainConfigs.ts index e287a46..c79d8f7 100644 --- a/scripts/printOnChainConfigs.ts +++ b/scripts/printOnChainConfigs.ts @@ -14,6 +14,8 @@ async function print(matchers: { chain: string[] }): Promise { backendAPI: { runtimeEnv: "prod", }, + stacks: {}, + btc: {}, brc20: {}, runes: {}, evm: { diff --git a/src/XLinkSDK.ts b/src/XLinkSDK.ts index 89ef81e..42e309f 100644 --- a/src/XLinkSDK.ts +++ b/src/XLinkSDK.ts @@ -1,7 +1,11 @@ import { Client } from "viem" import { getBTCPegInAddress } from "./bitcoinUtils/btcAddresses" import { nativeCurrencyAddress } from "./evmUtils/addressHelpers" -import { defaultEvmClients } from "./evmUtils/evmClients" +import { + defaultEvmClients, + evmChainIdFromKnownChainId, + evmChainIdToKnownChainId, +} from "./evmUtils/evmClients" import { getEVMContractCallInfo, getEVMToken, @@ -124,6 +128,15 @@ export interface XLinkSDKOptions { backendAPI?: { runtimeEnv?: "prod" | "dev" } + btc?: { + ignoreValidateResult?: boolean + } + brc20?: { + ignoreValidateResult?: boolean + } + runes?: { + ignoreValidateResult?: boolean + } } evm?: { /** @@ -160,11 +173,22 @@ export class XLinkSDK { ...options.__experimental?.backendAPI, runtimeEnv: options.__experimental?.backendAPI?.runtimeEnv ?? "prod", }, + stacks: { + tokensCache: new Map(), + }, + btc: { + ignoreValidateResult: + options.__experimental?.btc?.ignoreValidateResult ?? false, + }, brc20: { routesConfigCache: new Map(), + ignoreValidateResult: + options.__experimental?.brc20?.ignoreValidateResult ?? false, }, runes: { routesConfigCache: new Map(), + ignoreValidateResult: + options.__experimental?.runes?.ignoreValidateResult ?? false, }, evm: { onChainConfigCache: cacheEVMOnChainConfig ? new Map() : undefined, @@ -218,8 +242,18 @@ export class XLinkSDK { return checkingResult.some(r => r) } - stacksAddressFromStacksToken = stacksAddressFromStacksToken - stacksAddressToStacksToken = stacksAddressToStacksToken + stacksAddressFromStacksToken( + chain: ChainId, + token: KnownTokenId.StacksToken, + ): Promise { + return stacksAddressFromStacksToken(this.sdkContext, chain, token) + } + stacksAddressToStacksToken( + chain: ChainId, + address: StacksContractAddress, + ): Promise { + return stacksAddressToStacksToken(this.sdkContext, chain, address) + } /** * This function provides detailed information about token transfers from the Stacks network to other supported @@ -296,6 +330,18 @@ export class XLinkSDK { return } + async evmChainIdFromKnownChainId( + chain: KnownChainId.EVMChain, + ): Promise { + return evmChainIdFromKnownChainId(chain) + } + + async evmChainIdToKnownChainId( + chainId: bigint, + ): Promise { + return evmChainIdToKnownChainId(chainId) + } + /** * This function retrieves the contract address of a specific token on a given EVM-compatible blockchain. * @param chain - The ID of the EVM-compatible blockchain where the token contract is deployed. @@ -582,11 +628,12 @@ export class XLinkSDK { * or `undefined` if the chain is not a Stacks chain or if the contract address cannot be retrieved. */ async function stacksAddressFromStacksToken( + sdkContext: SDKGlobalContext, chain: ChainId, token: KnownTokenId.StacksToken, ): Promise { if (!KnownChainId.isStacksChain(chain)) return - const info = await getStacksTokenContractInfo(chain, token) + const info = await getStacksTokenContractInfo(sdkContext, chain, token) if (info == null) return return { deployerAddress: info.deployerAddress, @@ -604,11 +651,12 @@ async function stacksAddressFromStacksToken( * cannot be found. */ async function stacksAddressToStacksToken( + sdkContext: SDKGlobalContext, chain: ChainId, address: StacksContractAddress, ): Promise { if (!KnownChainId.isStacksChain(chain)) return - return getStacksToken(chain, address) + return getStacksToken(sdkContext, chain, address) } async function brc20TickFromBRC20Token( @@ -627,7 +675,8 @@ async function brc20TickToBRC20Token( ): Promise { if (!KnownChainId.isBRC20Chain(chain)) return const routes = await getBRC20SupportedRoutes(sdkContext, chain) - return routes.find(r => r.brc20Tick === tick)?.brc20Token + return routes.find(r => r.brc20Tick.toLowerCase() === tick.toLowerCase()) + ?.brc20Token } async function runesIdFromRunesToken( diff --git a/src/bitcoinUtils/apiHelpers/createBitcoinPegInRecipients.ts b/src/bitcoinUtils/apiHelpers/createBitcoinPegInRecipients.ts index f04af66..ba2598b 100644 --- a/src/bitcoinUtils/apiHelpers/createBitcoinPegInRecipients.ts +++ b/src/bitcoinUtils/apiHelpers/createBitcoinPegInRecipients.ts @@ -15,9 +15,17 @@ export async function createBitcoinPegInRecipients( sdkContext: Pick, info: { fromChain: KnownChainId.BitcoinChain - toChain: KnownChainId.StacksChain | KnownChainId.EVMChain fromToken: KnownTokenId.BitcoinToken - toToken: KnownTokenId.StacksToken | KnownTokenId.EVMToken + toChain: + | KnownChainId.StacksChain + | KnownChainId.EVMChain + | KnownChainId.BRC20Chain + | KnownChainId.RunesChain + toToken: + | KnownTokenId.StacksToken + | KnownTokenId.EVMToken + | KnownTokenId.BRC20Token + | KnownTokenId.RunesToken fromAddress: { address: string scriptPubKey: Uint8Array diff --git a/src/bitcoinUtils/peggingHelpers.ts b/src/bitcoinUtils/peggingHelpers.ts index cd0b3a9..3d071d2 100644 --- a/src/bitcoinUtils/peggingHelpers.ts +++ b/src/bitcoinUtils/peggingHelpers.ts @@ -1,8 +1,13 @@ -import { callReadOnlyFunction } from "@stacks/transactions" -import { CallReadOnlyFunctionFn } from "clarity-codegen" -import { fromCorrespondingStacksToken } from "../evmUtils/peggingHelpers" +import { evmTokenFromCorrespondingStacksToken } from "../evmUtils/peggingHelpers" import { getEVMTokenContractInfo } from "../evmUtils/xlinkContractHelpers" -import { stxTokenContractAddresses } from "../stacksUtils/stxContractAddresses" +import { + getBRC20SupportedRoutes, + getRunesSupportedRoutes, +} from "../metaUtils/xlinkContractHelpers" +import { + StacksContractName, + stxTokenContractAddresses, +} from "../stacksUtils/stxContractAddresses" import { executeReadonlyCallXLINK, getStacksContractCallInfo, @@ -13,53 +18,66 @@ import { IsSupportedFn, KnownRoute_FromBitcoin_ToStacks, KnownRoute_FromStacks_ToBitcoin, + KnownRoute_ToStacks, } from "../utils/buildSupportedRoutes" import { props } from "../utils/promiseHelpers" -import { checkNever } from "../utils/typeHelpers" -import { TransferProphet } from "../utils/types/TransferProphet" +import { + getFinalStepStacksTokenAddress, + SwapRoute, +} from "../utils/SwapRouteHelpers" +import { assertExclude, checkNever } from "../utils/typeHelpers" import { _allNoLongerSupportedEVMChains, KnownChainId, KnownTokenId, } from "../utils/types/knownIds" +import { TransferProphet } from "../utils/types/TransferProphet" import { getBTCPegInAddress } from "./btcAddresses" export const getBtc2StacksFeeInfo = async ( route: KnownRoute_FromBitcoin_ToStacks, + options: { + swapRoute: null | SwapRoute + }, ): Promise => { - const stacksContractCallInfo = getStacksContractCallInfo( + const stacksBaseContractCallInfo = getStacksContractCallInfo( route.toChain, - "btc-peg-in-endpoint-v2-05", + StacksContractName.BTCPegInEndpoint, ) - if (stacksContractCallInfo == null) return - - const executeOptions = { - deployerAddress: stacksContractCallInfo.deployerAddress, - callReadOnlyFunction: (callOptions => - callReadOnlyFunction({ - ...callOptions, - network: stacksContractCallInfo.network, - })) satisfies CallReadOnlyFunctionFn, + const stacksSwapContractCallInfo = getStacksContractCallInfo( + route.toChain, + StacksContractName.BTCPegInEndpointSwap, + ) + if ( + stacksBaseContractCallInfo == null || + stacksSwapContractCallInfo == null + ) { + return } + const contractCallInfo = + options.swapRoute == null + ? stacksBaseContractCallInfo + : stacksSwapContractCallInfo + const resp = await props({ isPaused: executeReadonlyCallXLINK( - stacksContractCallInfo.contractName, + contractCallInfo.contractName, "is-peg-in-paused", {}, - executeOptions, + contractCallInfo.executeOptions, ), feeRate: executeReadonlyCallXLINK( - stacksContractCallInfo.contractName, + contractCallInfo.contractName, "get-peg-in-fee", {}, - executeOptions, + contractCallInfo.executeOptions, ).then(numberFromStacksContractNumber), minFeeAmount: executeReadonlyCallXLINK( - stacksContractCallInfo.contractName, + contractCallInfo.contractName, "get-peg-in-min-fee", {}, - executeOptions, + contractCallInfo.executeOptions, ).then(numberFromStacksContractNumber), }) @@ -83,41 +101,103 @@ export const getBtc2StacksFeeInfo = async ( export const getStacks2BtcFeeInfo = async ( route: KnownRoute_FromStacks_ToBitcoin, + options: { + swappedFromRoute: null | KnownRoute_ToStacks + }, ): Promise => { const stacksContractCallInfo = getStacksContractCallInfo( route.fromChain, - "btc-peg-out-endpoint-v2-01", + StacksContractName.BTCPegOutEndpoint, ) - if (stacksContractCallInfo == null) return + const btcPegInSwapContractCallInfo = getStacksContractCallInfo( + route.fromChain, + StacksContractName.BTCPegInEndpointSwap, + ) + const metaPegInSwapContractCallInfo = getStacksContractCallInfo( + route.fromChain, + StacksContractName.MetaPegInEndpointSwap, + ) + if ( + stacksContractCallInfo == null || + btcPegInSwapContractCallInfo == null || + metaPegInSwapContractCallInfo == null + ) { + return + } - const executeOptions = { - deployerAddress: stacksContractCallInfo.deployerAddress, - callReadOnlyFunction: (callOptions => - callReadOnlyFunction({ - ...callOptions, - network: stacksContractCallInfo.network, - })) satisfies CallReadOnlyFunctionFn, + let feeInfo: + | undefined + | { + feeRate: Promise + minFeeAmount: Promise + } + if (options.swappedFromRoute != null) { + if (KnownChainId.isBitcoinChain(options.swappedFromRoute.fromChain)) { + feeInfo = { + feeRate: executeReadonlyCallXLINK( + btcPegInSwapContractCallInfo.contractName, + "get-btc-peg-out-fee", + {}, + btcPegInSwapContractCallInfo.executeOptions, + ).then(numberFromStacksContractNumber), + minFeeAmount: executeReadonlyCallXLINK( + btcPegInSwapContractCallInfo.contractName, + "get-btc-peg-out-min-fee", + {}, + btcPegInSwapContractCallInfo.executeOptions, + ).then(numberFromStacksContractNumber), + } + } else if ( + KnownChainId.isBRC20Chain(options.swappedFromRoute.fromChain) || + KnownChainId.isRunesChain(options.swappedFromRoute.fromChain) + ) { + feeInfo = { + feeRate: executeReadonlyCallXLINK( + metaPegInSwapContractCallInfo.contractName, + "get-btc-peg-out-fee", + {}, + metaPegInSwapContractCallInfo.executeOptions, + ).then(numberFromStacksContractNumber), + minFeeAmount: executeReadonlyCallXLINK( + metaPegInSwapContractCallInfo.contractName, + "get-btc-peg-out-min-fee", + {}, + metaPegInSwapContractCallInfo.executeOptions, + ).then(numberFromStacksContractNumber), + } + } else { + assertExclude( + options.swappedFromRoute.fromChain, + assertExclude.i(), + ) + checkNever(options.swappedFromRoute) + } + } + if (feeInfo == null) { + feeInfo = { + feeRate: executeReadonlyCallXLINK( + stacksContractCallInfo.contractName, + "get-peg-out-fee", + {}, + stacksContractCallInfo.executeOptions, + ).then(numberFromStacksContractNumber), + minFeeAmount: executeReadonlyCallXLINK( + stacksContractCallInfo.contractName, + "get-peg-out-min-fee", + {}, + stacksContractCallInfo.executeOptions, + ).then(numberFromStacksContractNumber), + } } const resp = await props({ + ...feeInfo, isPaused: executeReadonlyCallXLINK( stacksContractCallInfo.contractName, "is-peg-out-paused", {}, - executeOptions, + stacksContractCallInfo.executeOptions, ), - feeRate: executeReadonlyCallXLINK( - stacksContractCallInfo.contractName, - "get-peg-out-fee", - {}, - executeOptions, - ).then(numberFromStacksContractNumber), - minFeeAmount: executeReadonlyCallXLINK( - stacksContractCallInfo.contractName, - "get-peg-out-min-fee", - {}, - executeOptions, - ).then(numberFromStacksContractNumber), }) return { @@ -172,21 +252,25 @@ export const isSupportedBitcoinRoute: IsSupportedFn = async (ctx, route) => { if (KnownChainId.isBitcoinChain(toChain)) { return false } - if (KnownChainId.isRunesChain(toChain)) { - return false - } - if (KnownChainId.isBRC20Chain(toChain)) { - return false - } + + const finalStepStacksToken = + route.swapRoute == null + ? KnownTokenId.Stacks.aBTC + : await getFinalStepStacksTokenAddress(ctx, { + swap: route.swapRoute, + stacksChain: + fromChain === KnownChainId.Bitcoin.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet, + }) if (KnownChainId.isStacksChain(toChain)) { if (!KnownTokenId.isStacksToken(toToken)) return false - return ( - fromToken === KnownTokenId.Bitcoin.BTC && - toToken === KnownTokenId.Stacks.aBTC && - stxTokenContractAddresses[toToken]?.[toChain] != null - ) + if (fromToken !== KnownTokenId.Bitcoin.BTC) return false + if (stxTokenContractAddresses[toToken]?.[toChain] == null) return false + + return toToken === finalStepStacksToken } if (KnownChainId.isEVMChain(toChain)) { @@ -195,11 +279,30 @@ export const isSupportedBitcoinRoute: IsSupportedFn = async (ctx, route) => { const info = await getEVMTokenContractInfo(ctx, toChain, toToken) if (info == null) return false - const toEVMTokens = await fromCorrespondingStacksToken( + if (finalStepStacksToken == null) return false + + return evmTokenFromCorrespondingStacksToken( toChain, - KnownTokenId.Stacks.aBTC, + finalStepStacksToken, + ).then(toEVMTokens => toEVMTokens.includes(toToken)) + } + + if (KnownChainId.isRunesChain(toChain)) { + const routes = await getRunesSupportedRoutes(ctx, toChain) + return routes.some( + route => + route.runesToken === toToken && + route.stacksToken === finalStepStacksToken, + ) + } + + if (KnownChainId.isBRC20Chain(toChain)) { + const routes = await getBRC20SupportedRoutes(ctx, toChain) + return routes.some( + route => + route.brc20Token === toToken && + route.stacksToken === finalStepStacksToken, ) - return toEVMTokens.includes(toToken as any) } checkNever(toChain) diff --git a/src/config.ts b/src/config.ts index f3f00af..cc5f5f1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -8,3 +8,17 @@ export const STACKS_MAINNET = new StacksMainnet({ export const STACKS_TESTNET = new StacksMocknet({ url: "https://nakamoto-dev-api.alexlab.co", }) + +export const envName = process.env.ENV_NAME === "dev" ? "dev" : "prod" +export const contractNameOverrides: Record = + envName === "prod" + ? { + "btc-peg-in-endpoint-v2-07-swap": "btc-peg-in-v2-07-swap", + "meta-peg-in-endpoint-v2-06-swap": "meta-peg-in-v2-06-swap", + } + : { + "btc-peg-in-endpoint-v2-07-swap": "btc-peg-in-v2-07-swap-01", + "meta-peg-in-endpoint-v2-06-swap": "meta-peg-in-v2-06-swap-01", + "meta-peg-out-endpoint-v2-04": "meta-peg-out-endpoint-v2-04-da", + "token-wvliabtc": "token-wvliabtc-da", + } diff --git a/src/evmUtils/evmClients.ts b/src/evmUtils/evmClients.ts index 6f77351..af06aeb 100644 --- a/src/evmUtils/evmClients.ts +++ b/src/evmUtils/evmClients.ts @@ -25,129 +25,146 @@ import { xLayer, } from "./evmChainInfos" import { EVMChain } from "./evmContractAddresses" +import { KnownChainId } from "../utils/types/knownIds" +import { entries } from "../utils/objectHelper" -export const defaultEvmClients: Record< - Exclude, - Client -> = { - [EVMChain.Ethereum]: createClient({ - chain: mainnet, - transport: http(), - batch: { multicall: true }, - }), - [EVMChain.Sepolia]: createClient({ - chain: sepolia, - transport: http(), - batch: { multicall: true }, - }), +export const defaultEvmClients: Partial> = + { + [EVMChain.Ethereum]: createClient({ + chain: mainnet, + transport: http(), + batch: { multicall: true }, + }), + [EVMChain.Sepolia]: createClient({ + chain: sepolia, + transport: http(), + batch: { multicall: true }, + }), - [EVMChain.BSC]: createClient({ - chain: bsc, - transport: http(), - batch: { multicall: true }, - }), - [EVMChain.BSCTestnet]: createClient({ - chain: bscTestnet, - transport: http(), - batch: { multicall: true }, - }), + [EVMChain.BSC]: createClient({ + chain: bsc, + transport: http(), + batch: { multicall: true }, + }), + [EVMChain.BSCTestnet]: createClient({ + chain: bscTestnet, + transport: http(), + batch: { multicall: true }, + }), - [EVMChain.CoreDAO]: createClient({ - chain: coreDao, - transport: http(), - batch: { multicall: true }, - }), - [EVMChain.CoreDAOTestnet]: createClient({ - chain: coreDaoTestnet, - transport: http(), - batch: { multicall: true }, - }), + [EVMChain.CoreDAO]: createClient({ + chain: coreDao, + transport: http(), + batch: { multicall: true }, + }), + [EVMChain.CoreDAOTestnet]: createClient({ + chain: coreDaoTestnet, + transport: http(), + batch: { multicall: true }, + }), - [EVMChain.Bsquared]: createClient({ - chain: bsquared, - transport: http(), - batch: { multicall: true }, - }), + [EVMChain.Bsquared]: createClient({ + chain: bsquared, + transport: http(), + batch: { multicall: true }, + }), - [EVMChain.BOB]: createClient({ - chain: bob, - transport: http(), - batch: { multicall: true }, - }), + [EVMChain.BOB]: createClient({ + chain: bob, + transport: http(), + batch: { multicall: true }, + }), - [EVMChain.Bitlayer]: createClient({ - chain: bitlayer, - transport: http(), - batch: { multicall: true }, - }), + [EVMChain.Bitlayer]: createClient({ + chain: bitlayer, + transport: http(), + batch: { multicall: true }, + }), - [EVMChain.Lorenzo]: createClient({ - chain: lorenzo, - transport: http(), - batch: { multicall: true }, - }), + [EVMChain.Lorenzo]: createClient({ + chain: lorenzo, + transport: http(), + batch: { multicall: true }, + }), - [EVMChain.Merlin]: createClient({ - chain: merlin, - transport: http(), - batch: { multicall: true }, - }), + [EVMChain.Merlin]: createClient({ + chain: merlin, + transport: http(), + batch: { multicall: true }, + }), - [EVMChain.AILayer]: createClient({ - chain: ailayer, - transport: http(), - batch: { multicall: true }, - }), + [EVMChain.AILayer]: createClient({ + chain: ailayer, + transport: http(), + batch: { multicall: true }, + }), - [EVMChain.Mode]: createClient({ - chain: mode, - transport: http(), - batch: { multicall: true }, - }), + [EVMChain.Mode]: createClient({ + chain: mode, + transport: http(), + batch: { multicall: true }, + }), - [EVMChain.XLayer]: createClient({ - chain: xLayer, - transport: fallback([http(), http("https://xlayerrpc.okx.com")]), - batch: { multicall: true }, - }), + [EVMChain.XLayer]: createClient({ + chain: xLayer, + transport: fallback([http(), http("https://xlayerrpc.okx.com")]), + batch: { multicall: true }, + }), - [EVMChain.Arbitrum]: createClient({ - chain: arbitrum, - transport: http(), - batch: { multicall: true }, - }), + [EVMChain.Arbitrum]: createClient({ + chain: arbitrum, + transport: http(), + batch: { multicall: true }, + }), - [EVMChain.Aurora]: createClient({ - chain: aurora, - transport: http(), - batch: { multicall: true }, - }), + [EVMChain.Aurora]: createClient({ + chain: aurora, + transport: http(), + batch: { multicall: true }, + }), - [EVMChain.Manta]: createClient({ - chain: manta, - transport: http(), - batch: { multicall: true }, - }), + [EVMChain.Manta]: createClient({ + chain: manta, + transport: http(), + batch: { multicall: true }, + }), - [EVMChain.Linea]: createClient({ - chain: linea, - transport: http(), - batch: { multicall: true }, - }), + [EVMChain.Linea]: createClient({ + chain: linea, + transport: http(), + batch: { multicall: true }, + }), - [EVMChain.Base]: createClient({ - chain: base, - transport: http(), - batch: { multicall: true }, - }), + [EVMChain.Base]: createClient({ + chain: base, + transport: http(), + batch: { multicall: true }, + }), - [EVMChain.BlifeTestnet]: createClient({ - chain: blifeTestnet, - transport: http(), - }), + [EVMChain.BlifeTestnet]: createClient({ + chain: blifeTestnet, + transport: http(), + }), - [EVMChain.BeraTestnet]: createClient({ - chain: berachainTestnet, - transport: http(), - }), + [EVMChain.BeraTestnet]: createClient({ + chain: berachainTestnet, + transport: http(), + batch: { multicall: true }, + }), + } satisfies Record, Client> + +export const evmChainIdFromKnownChainId = ( + chain: KnownChainId.EVMChain, +): undefined | bigint => { + const client = defaultEvmClients[chain] + const chainId = client?.chain?.id + if (chainId == null) return + return BigInt(chainId) +} +export const evmChainIdToKnownChainId = ( + chainId: bigint, +): undefined | KnownChainId.EVMChain => { + return entries(defaultEvmClients).find( + ([_, client]) => client?.chain?.id === Number(chainId), + )?.[0] } diff --git a/src/evmUtils/peggingHelpers.ts b/src/evmUtils/peggingHelpers.ts index fa3a0d8..1894a5c 100644 --- a/src/evmUtils/peggingHelpers.ts +++ b/src/evmUtils/peggingHelpers.ts @@ -1,5 +1,4 @@ -import { callReadOnlyFunction } from "@stacks/transactions" -import { CallReadOnlyFunctionFn, unwrapResponse } from "clarity-codegen" +import { unwrapResponse } from "clarity-codegen" import { readContract } from "viem/actions" import { getBRC20SupportedRoutes, @@ -8,6 +7,7 @@ import { import { contractAssignedChainIdFromKnownChain } from "../stacksUtils/crossContractDataMapping" import { getTerminatingStacksTokenContractAddress, + StacksContractName, stxTokenContractAddresses, } from "../stacksUtils/stxContractAddresses" import { @@ -46,7 +46,7 @@ export const getEvm2StacksFeeInfo = async ( ): Promise => { const stacksContractCallInfo = getStacksContractCallInfo( route.toChain, - "cross-peg-in-endpoint-v2-04", + StacksContractName.EVMPegInEndpoint, ) const evmContractCallInfo = await getEVMContractCallInfo(ctx, route.fromChain) const evmTokenContractCallInfo = await getEVMTokenContractInfo( @@ -66,15 +66,6 @@ export const getEvm2StacksFeeInfo = async ( return getEvm2StacksNativeBridgeFeeInfo(ctx, route) } - const executeOptions = { - deployerAddress: stacksContractCallInfo.deployerAddress, - callReadOnlyFunction: (callOptions => - callReadOnlyFunction({ - ...callOptions, - network: stacksContractCallInfo.network, - })) satisfies CallReadOnlyFunctionFn, - } - const { client, tokenContractAddress } = evmTokenContractCallInfo const registryAddr = @@ -126,7 +117,7 @@ export const getEvm2StacksFeeInfo = async ( stacksContractCallInfo.contractName, "get-paused", {}, - executeOptions, + stacksContractCallInfo.executeOptions, ), }) @@ -157,7 +148,7 @@ const getEvm2StacksNativeBridgeFeeInfo = async ( ): Promise => { const stacksContractCallInfo = getStacksContractCallInfo( route.toChain, - "cross-peg-in-endpoint-v2-04", + StacksContractName.EVMPegInEndpoint, ) const evmContractCallInfo = await getEVMContractCallInfo(ctx, route.fromChain) if ( @@ -167,21 +158,12 @@ const getEvm2StacksNativeBridgeFeeInfo = async ( return } - const executeOptions = { - deployerAddress: stacksContractCallInfo.deployerAddress, - callReadOnlyFunction: (callOptions => - callReadOnlyFunction({ - ...callOptions, - network: stacksContractCallInfo.network, - })) satisfies CallReadOnlyFunctionFn, - } - const resp = await props({ isPaused: executeReadonlyCallXLINK( stacksContractCallInfo.contractName, "get-paused", {}, - executeOptions, + stacksContractCallInfo.executeOptions, ), }) @@ -195,13 +177,15 @@ const getEvm2StacksNativeBridgeFeeInfo = async ( } export const getStacks2EvmFeeInfo = async ( + ctx: SDKGlobalContext, route: KnownRoute_FromStacks_ToEVM, ): Promise => { const stacksContractCallInfo = getStacksContractCallInfo( route.fromChain, - "cross-peg-out-endpoint-v2-01", + StacksContractName.EVMPegOutEndpoint, ) - const stacksTokenContractCallInfo = getStacksTokenContractInfo( + const stacksTokenContractCallInfo = await getStacksTokenContractInfo( + ctx, route.fromChain, route.fromToken, ) @@ -214,15 +198,6 @@ export const getStacks2EvmFeeInfo = async ( getTerminatingStacksTokenContractAddress(route) ?? stacksTokenContractCallInfo - const executeOptions = { - deployerAddress: stacksContractCallInfo.deployerAddress, - callReadOnlyFunction: (callOptions => - callReadOnlyFunction({ - ...callOptions, - network: stacksContractCallInfo.network, - })) satisfies CallReadOnlyFunctionFn, - } - const tokenConf = await Promise.all([ executeReadonlyCallXLINK( stacksContractCallInfo.contractName, @@ -233,13 +208,13 @@ export const getStacks2EvmFeeInfo = async ( "chain-id": toChainId, }, }, - executeOptions, + stacksContractCallInfo.executeOptions, ), executeReadonlyCallXLINK( stacksContractCallInfo.contractName, "get-paused", {}, - executeOptions, + stacksContractCallInfo.executeOptions, ), ]).then(([resp, isPaused]) => { if (resp.type !== "success") return undefined @@ -281,7 +256,7 @@ export const getStacks2EvmFeeInfo = async ( } } -export async function fromCorrespondingStacksToken( +export async function evmTokenFromCorrespondingStacksToken( toChain: KnownChainId.EVMChain, stacksToken: KnownTokenId.StacksToken, ): Promise { @@ -348,7 +323,7 @@ export async function fromCorrespondingStacksToken( checkNever(restEVMTokenPossibilities) return [] } -export async function toCorrespondingStacksToken( +export async function evmTokenToCorrespondingStacksToken( evmToken: KnownTokenId.EVMToken, ): Promise { const EVMToken = KnownTokenId.EVM @@ -429,7 +404,7 @@ export const isSupportedEVMRoute: IsSupportedFn = async (ctx, route) => { if (KnownChainId.isStacksChain(toChain)) { if (!KnownTokenId.isStacksToken(toToken)) return false - const stacksToken = await toCorrespondingStacksToken(fromToken) + const stacksToken = await evmTokenToCorrespondingStacksToken(fromToken) if (stacksToken == null) return false if (stxTokenContractAddresses[stacksToken]?.[toChain] == null) { @@ -445,10 +420,11 @@ export const isSupportedEVMRoute: IsSupportedFn = async (ctx, route) => { const toTokenInfo = await getEVMTokenContractInfo(ctx, toChain, toToken) if (toTokenInfo == null) return false - const transitStacksToken = await toCorrespondingStacksToken(fromToken) + const transitStacksToken = + await evmTokenToCorrespondingStacksToken(fromToken) if (transitStacksToken == null) return false - const toEVMTokens = await fromCorrespondingStacksToken( + const toEVMTokens = await evmTokenFromCorrespondingStacksToken( toChain, transitStacksToken, ) @@ -457,14 +433,15 @@ export const isSupportedEVMRoute: IsSupportedFn = async (ctx, route) => { if (KnownChainId.isBitcoinChain(toChain)) { if (!KnownTokenId.isBitcoinToken(toToken)) return false - const stacksToken = await toCorrespondingStacksToken(fromToken) + const stacksToken = await evmTokenToCorrespondingStacksToken(fromToken) return stacksToken === KnownTokenId.Stacks.aBTC } if (KnownChainId.isRunesChain(toChain)) { if (!KnownTokenId.isRunesToken(toToken)) return false - const transitStacksToken = await toCorrespondingStacksToken(fromToken) + const transitStacksToken = + await evmTokenToCorrespondingStacksToken(fromToken) if (transitStacksToken == null) return false const runesRoutes = await getRunesSupportedRoutes(ctx, toChain) @@ -474,7 +451,8 @@ export const isSupportedEVMRoute: IsSupportedFn = async (ctx, route) => { if (KnownChainId.isBRC20Chain(toChain)) { if (!KnownTokenId.isBRC20Token(toToken)) return false - const transitStacksToken = await toCorrespondingStacksToken(fromToken) + const transitStacksToken = + await evmTokenToCorrespondingStacksToken(fromToken) if (transitStacksToken == null) return false const brc20Routes = await getBRC20SupportedRoutes(ctx, toChain) diff --git a/src/evmUtils/xlinkContractHelpers.ts b/src/evmUtils/xlinkContractHelpers.ts index 64bfd89..9c9ffe1 100644 --- a/src/evmUtils/xlinkContractHelpers.ts +++ b/src/evmUtils/xlinkContractHelpers.ts @@ -156,7 +156,7 @@ const getOnChainConfigs = async ( configContractAddress: Address, ): Promise => { const cache = sdkContext.evm.onChainConfigCache - const cacheKey = `${chain}:${configContractAddress}` + const cacheKey = `${chain}:${configContractAddress}` as const if (cache != null) { const cachedPromise = cache.get(cacheKey) diff --git a/src/index.ts b/src/index.ts index 8a317f8..5239a00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,11 @@ export { StacksContractAddress, PublicEVMContractType as EVMContractType, } from "./xlinkSdkUtils/types" +export { + SwapRoute, + SwapRoute_WithMinimumAmountsToReceive_Public as SwapRoute_WithMinimumAmountsOut, + SwapRoute_WithExchangeRate_Public as SwapRoute_WithExchangeRate, +} from "./utils/SwapRouteHelpers" export { TimeLockedAsset } from "./xlinkSdkUtils/timelockFromEVM" export { PublicTransferProphet as TransferProphet, diff --git a/src/lowlevelUnstableInfos.ts b/src/lowlevelUnstableInfos.ts index 4feb9cb..188a582 100644 --- a/src/lowlevelUnstableInfos.ts +++ b/src/lowlevelUnstableInfos.ts @@ -4,9 +4,13 @@ */ import { - fromCorrespondingStacksToken, - toCorrespondingStacksToken, + evmTokenFromCorrespondingStacksToken, + evmTokenToCorrespondingStacksToken, } from "./evmUtils/peggingHelpers" +import { + metaTokenFromCorrespondingStacksToken, + metaTokenToCorrespondingStacksToken, +} from "./metaUtils/peggingHelpers" import { KnownChainId, KnownTokenId } from "./utils/types/knownIds" import { SDKGlobalContext } from "./xlinkSdkUtils/types.internal" @@ -16,22 +20,31 @@ export { } from "./stacksUtils/crossContractDataMapping" export { - fromCorrespondingStacksToken as evmTokensFromCorrespondingStacksToken, - toCorrespondingStacksToken as evmTokenToCorrespondingStacksToken, -} - -export { - getTerminatingStacksTokenContractAddress, getEVMTokenIdFromTerminatingStacksTokenContractAddress, + getTerminatingStacksTokenContractAddress, } from "./stacksUtils/stxContractAddresses" -export { - addressFromBuffer, - addressToBuffer, -} from "./stacksUtils/xlinkContractHelpers" - export { isSupportedMetaRoute } from "./metaUtils/peggingHelpers" +export { + bridgeInfoFromMeta, + BridgeInfoFromMetaInput, + BridgeInfoFromMetaOutput, +} from "./xlinkSdkUtils/bridgeInfoFromMeta" +export { + brc20TokenFromTick, + brc20TokenToTick, + runesTokenFromId, + runesTokenToId, +} from "./metaUtils/tokenAddresses" + +export { addressFromBuffer, addressToBuffer } from "./utils/addressHelpers" + +export { + BridgeSwapRouteNode, + CreateBridgeOrderResult, + createBridgeOrderFromBitcoin, +} from "./stacksUtils/createBridgeOrderFromBitcoin" export { bridgeFromEVM_toLaunchpad } from "./xlinkSdkUtils/bridgeFromEVM" export const getXLinkSDKContext = ( @@ -46,7 +59,7 @@ export const evmTokensFromStacksToken = async (options: { }): Promise<{ evmTokens: KnownTokenId.EVMToken[] }> => { - const evmTokens = await fromCorrespondingStacksToken( + const evmTokens = await evmTokenFromCorrespondingStacksToken( options.toEVMChain, options.fromStacksToken, ) @@ -58,6 +71,44 @@ export const evmTokenToStacksToken = async (options: { }): Promise<{ stacksTokens: KnownTokenId.StacksToken[] }> => { - const stacksTokens = await toCorrespondingStacksToken(options.fromEVMToken) + const stacksTokens = await evmTokenToCorrespondingStacksToken( + options.fromEVMToken, + ) + return { stacksTokens: stacksTokens == null ? [] : [stacksTokens] } +} + +export const metaTokensFromStacksToken = async ( + sdk: import("./XLinkSDK").XLinkSDK, + options: { + fromStacksToken: KnownTokenId.StacksToken + toChain: KnownChainId.BRC20Chain | KnownChainId.RunesChain + }, +): Promise<{ + tokens: (KnownTokenId.BRC20Token | KnownTokenId.RunesToken)[] +}> => { + const metaTokens = await metaTokenFromCorrespondingStacksToken( + getXLinkSDKContext(sdk), + options.toChain, + options.fromStacksToken, + ) + return { tokens: metaTokens == null ? [] : [metaTokens as any] } +} +export const metaTokenToStacksToken = async ( + sdk: import("./XLinkSDK").XLinkSDK, + options: { + fromChain: KnownChainId.BRC20Chain | KnownChainId.RunesChain + fromToken: KnownTokenId.BRC20Token | KnownTokenId.RunesToken + toStacksChain: KnownChainId.StacksChain + }, +): Promise<{ + stacksTokens: KnownTokenId.StacksToken[] +}> => { + const stacksTokens = await metaTokenToCorrespondingStacksToken( + getXLinkSDKContext(sdk), + { + chain: options.fromChain as any, + token: options.fromToken as any, + }, + ) return { stacksTokens: stacksTokens == null ? [] : [stacksTokens] } } diff --git a/src/metaUtils/peggingHelpers.ts b/src/metaUtils/peggingHelpers.ts index 1488e6d..8c22cb1 100644 --- a/src/metaUtils/peggingHelpers.ts +++ b/src/metaUtils/peggingHelpers.ts @@ -1,13 +1,25 @@ -import { toCorrespondingStacksToken } from "../evmUtils/peggingHelpers" +import { evmTokenToCorrespondingStacksToken } from "../evmUtils/peggingHelpers" import { getEVMTokenContractInfo } from "../evmUtils/xlinkContractHelpers" +import { StacksContractName } from "../stacksUtils/stxContractAddresses" +import { + executeReadonlyCallXLINK, + getStacksContractCallInfo, + numberFromStacksContractNumber, +} from "../stacksUtils/xlinkContractHelpers" import { BigNumber } from "../utils/BigNumber" import { - _KnownRoute_FromBRC20_ToStacks, - _KnownRoute_FromRunes_ToStacks, + getFinalStepStacksTokenAddress, + getFirstStepStacksTokenAddress, + SwapRoute, +} from "../utils/SwapRouteHelpers" +import { IsSupportedFn, + KnownRoute_FromBRC20_ToStacks, + KnownRoute_FromRunes_ToStacks, KnownRoute_FromStacks_ToBRC20, KnownRoute_FromStacks_ToRunes, } from "../utils/buildSupportedRoutes" +import { props } from "../utils/promiseHelpers" import { checkNever, isNotNull } from "../utils/typeHelpers" import { TransferProphet } from "../utils/types/TransferProphet" import { @@ -18,15 +30,62 @@ import { import { SDKGlobalContext } from "../xlinkSdkUtils/types.internal" import { getMetaPegInAddress } from "./btcAddresses" import { - BRC20SupportedRoute, getBRC20SupportedRoutes, getRunesSupportedRoutes, - RunesSupportedRoute, } from "./xlinkContractHelpers" +export async function metaTokenFromCorrespondingStacksToken( + ctx: SDKGlobalContext, + chain: KnownChainId.BRC20Chain | KnownChainId.RunesChain, + stacksToken: KnownTokenId.StacksToken, +): Promise { + if (KnownChainId.isBRC20Chain(chain)) { + const routes = await getBRC20SupportedRoutes(ctx, chain) + return routes.find(r => r.stacksToken === stacksToken)?.brc20Token + } else if (KnownChainId.isRunesChain(chain)) { + const routes = await getRunesSupportedRoutes(ctx, chain) + return routes.find(r => r.stacksToken === stacksToken)?.runesToken + } else { + checkNever(chain) + return + } +} + +export async function metaTokenToCorrespondingStacksToken( + ctx: SDKGlobalContext, + route: + | { chain: KnownChainId.BRC20Chain; token: KnownTokenId.BRC20Token } + | { chain: KnownChainId.RunesChain; token: KnownTokenId.RunesToken }, +): Promise { + if (KnownChainId.isBRC20Chain(route.chain)) { + const routes = await getBRC20SupportedRoutes(ctx, route.chain) + return routes.find(r => r.brc20Token === route.token)?.stacksToken + } else if (KnownChainId.isRunesChain(route.chain)) { + const routes = await getRunesSupportedRoutes(ctx, route.chain) + return routes.find(r => r.runesToken === route.token)?.stacksToken + } else { + checkNever(route.chain) + return + } +} + export const getMeta2StacksFeeInfo = async ( ctx: SDKGlobalContext, - route: _KnownRoute_FromBRC20_ToStacks | _KnownRoute_FromRunes_ToStacks, + route: KnownRoute_FromBRC20_ToStacks | KnownRoute_FromRunes_ToStacks, + options: { + swapRoute: null | SwapRoute + }, +): Promise => { + if (options.swapRoute != null) { + return getMeta2StacksSwapFeeInfo(route) + } else { + return getMeta2StacksBaseFeeInfo(ctx, route) + } +} + +const getMeta2StacksBaseFeeInfo = async ( + ctx: SDKGlobalContext, + route: KnownRoute_FromBRC20_ToStacks | KnownRoute_FromRunes_ToStacks, ): Promise => { const filteredRoutes = KnownChainId.isBRC20Chain(route.fromChain) ? await getBRC20SupportedRoutes(ctx, route.fromChain).then(routes => @@ -60,7 +119,7 @@ export const getMeta2StacksFeeInfo = async ( ? null : { type: "fixed" as const, - token: KnownTokenId.Stacks.aBTC, + token: KnownTokenId.Bitcoin.BTC, amount: filteredRoute.pegInFeeBitcoinAmount, }, ].filter(isNotNull), @@ -69,6 +128,47 @@ export const getMeta2StacksFeeInfo = async ( } } +const getMeta2StacksSwapFeeInfo = async ( + route1: KnownRoute_FromBRC20_ToStacks | KnownRoute_FromRunes_ToStacks, +): Promise => { + const stacksSwapContractCallInfo = getStacksContractCallInfo( + route1.toChain, + StacksContractName.MetaPegInEndpointSwap, + ) + if (stacksSwapContractCallInfo == null) { + return + } + + const resp = await props({ + isPaused: executeReadonlyCallXLINK( + stacksSwapContractCallInfo.contractName, + "is-paused", + {}, + stacksSwapContractCallInfo.executeOptions, + ), + fixedBtcFee: executeReadonlyCallXLINK( + stacksSwapContractCallInfo.contractName, + "get-peg-in-fee", + {}, + stacksSwapContractCallInfo.executeOptions, + ).then(numberFromStacksContractNumber), + }) + + return { + isPaused: resp.isPaused, + bridgeToken: route1.fromToken, + fees: [ + { + type: "fixed" as const, + token: KnownTokenId.Bitcoin.BTC, + amount: resp.fixedBtcFee, + }, + ], + minBridgeAmount: BigNumber.ZERO, + maxBridgeAmount: null, + } +} + export const getStacks2MetaFeeInfo = async ( ctx: SDKGlobalContext, route: KnownRoute_FromStacks_ToBRC20 | KnownRoute_FromStacks_ToRunes, @@ -105,7 +205,7 @@ export const getStacks2MetaFeeInfo = async ( ? null : { type: "fixed" as const, - token: KnownTokenId.Stacks.aBTC, + token: KnownTokenId.Bitcoin.BTC, amount: filteredRoute.pegOutFeeBitcoinAmount, }, ].filter(isNotNull), @@ -158,13 +258,23 @@ export const isSupportedMetaRoute: IsSupportedFn = async (ctx, route) => { if (!KnownTokenId.isStacksToken(toToken)) return false if (KnownChainId.isRunesChain(fromChain)) { + if (!KnownTokenId.isRunesToken(fromToken)) return false + const runesRoutes = await getRunesSupportedRoutes(ctx, fromChain) - return runesRoutes.some(route => route.stacksToken === toToken) + return runesRoutes.some( + route => + route.runesToken === fromToken && route.stacksToken === toToken, + ) } if (KnownChainId.isBRC20Chain(fromChain)) { + if (!KnownTokenId.isBRC20Token(fromToken)) return false + const brc20Routes = await getBRC20SupportedRoutes(ctx, fromChain) - return brc20Routes.some(route => route.stacksToken === toToken) + return brc20Routes.some( + route => + route.brc20Token === fromToken && route.stacksToken === toToken, + ) } checkNever(fromChain) @@ -177,17 +287,29 @@ export const isSupportedMetaRoute: IsSupportedFn = async (ctx, route) => { const info = await getEVMTokenContractInfo(ctx, toChain, toToken) if (info == null) return false - const transitStacksToken = await toCorrespondingStacksToken(toToken) + const transitStacksToken = await evmTokenToCorrespondingStacksToken(toToken) if (transitStacksToken == null) return false if (KnownChainId.isRunesChain(fromChain)) { + if (!KnownTokenId.isRunesToken(fromToken)) return false + const runesRoutes = await getRunesSupportedRoutes(ctx, fromChain) - return runesRoutes.some(route => route.stacksToken === transitStacksToken) + return runesRoutes.some( + route => + route.runesToken === fromToken && + route.stacksToken === transitStacksToken, + ) } if (KnownChainId.isBRC20Chain(fromChain)) { + if (!KnownTokenId.isBRC20Token(fromToken)) return false + const brc20Routes = await getBRC20SupportedRoutes(ctx, fromChain) - return brc20Routes.some(route => route.stacksToken === transitStacksToken) + return brc20Routes.some( + route => + route.brc20Token === fromToken && + route.stacksToken === transitStacksToken, + ) } checkNever(fromChain) @@ -197,39 +319,205 @@ export const isSupportedMetaRoute: IsSupportedFn = async (ctx, route) => { if (KnownChainId.isRunesChain(toChain)) { if (!KnownTokenId.isRunesToken(toToken)) return false - // TODO: runes -> runes (swap) is not supported yet - if (KnownChainId.isRunesChain(fromChain)) return false + const finalStepStacksToken = + route.swapRoute == null + ? await metaTokenToCorrespondingStacksToken(ctx, { + chain: toChain, + token: toToken, + }) + : await getFinalStepStacksTokenAddress(ctx, { + swap: route.swapRoute, + stacksChain: + toChain === KnownChainId.Runes.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet, + }) + if (finalStepStacksToken == null) return false - const brc20Routes = await getBRC20SupportedRoutes(ctx, fromChain) - const runesRoutes = await getRunesSupportedRoutes(ctx, toChain) - return getRoutesOverlapping(brc20Routes, runesRoutes) != null + // runes -> runes + if (KnownChainId.isRunesChain(fromChain)) { + if (!KnownTokenId.isRunesToken(fromToken)) return false + + const firstStepStacksToken = + route.swapRoute == null + ? await metaTokenToCorrespondingStacksToken(ctx, { + chain: fromChain, + token: fromToken, + }) + : await getFirstStepStacksTokenAddress(ctx, { + swap: route.swapRoute, + stacksChain: + fromChain === KnownChainId.Runes.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet, + }) + if (firstStepStacksToken == null) return false + + if (route.swapRoute == null) { + return firstStepStacksToken === finalStepStacksToken + } + + const runesRoutes = await getRunesSupportedRoutes(ctx, fromChain) + + return ( + runesRoutes.find( + route => + route.runesToken === fromToken && + route.stacksToken === firstStepStacksToken, + ) != null && + runesRoutes.find( + route => + route.runesToken === toToken && + route.stacksToken === finalStepStacksToken, + ) != null + ) + } + + // brc20 -> runes + if (KnownChainId.isBRC20Chain(fromChain)) { + if (!KnownTokenId.isBRC20Token(fromToken)) return false + + const firstStepStacksToken = + route.swapRoute == null + ? await metaTokenToCorrespondingStacksToken(ctx, { + chain: fromChain, + token: fromToken, + }) + : await getFirstStepStacksTokenAddress(ctx, { + swap: route.swapRoute, + stacksChain: + fromChain === KnownChainId.BRC20.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet, + }) + if (firstStepStacksToken == null) return false + + if (route.swapRoute == null) { + return firstStepStacksToken === finalStepStacksToken + } + + const fromRoutes = await getBRC20SupportedRoutes(ctx, fromChain) + const toRoutes = await getRunesSupportedRoutes(ctx, toChain) + + return ( + fromRoutes.find( + route => + route.brc20Token === fromToken && + route.stacksToken === firstStepStacksToken, + ) != null && + toRoutes.find( + route => + route.runesToken === toToken && + route.stacksToken === finalStepStacksToken, + ) != null + ) + } + + checkNever(fromChain) + return false } if (KnownChainId.isBRC20Chain(toChain)) { if (!KnownTokenId.isBRC20Token(toToken)) return false - // TODO: brc20 -> brc20 (swap) is not supported yet - if (KnownChainId.isBRC20Chain(fromChain)) return false + const finalStepStacksToken = + route.swapRoute == null + ? await metaTokenToCorrespondingStacksToken(ctx, { + chain: toChain, + token: toToken, + }) + : await getFinalStepStacksTokenAddress(ctx, { + swap: route.swapRoute, + stacksChain: + toChain === KnownChainId.BRC20.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet, + }) + if (finalStepStacksToken == null) return false - const brc20Routes = await getBRC20SupportedRoutes(ctx, toChain) - const runesRoutes = await getRunesSupportedRoutes(ctx, fromChain) - return getRoutesOverlapping(brc20Routes, runesRoutes) != null + // brc20 -> brc20 + if (KnownChainId.isBRC20Chain(fromChain)) { + if (!KnownTokenId.isBRC20Token(fromToken)) return false + + const firstStepStacksToken = + route.swapRoute == null + ? await metaTokenToCorrespondingStacksToken(ctx, { + chain: fromChain, + token: fromToken, + }) + : await getFirstStepStacksTokenAddress(ctx, { + swap: route.swapRoute, + stacksChain: + fromChain === KnownChainId.BRC20.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet, + }) + if (firstStepStacksToken == null) return false + + if (route.swapRoute == null) { + return firstStepStacksToken === finalStepStacksToken + } + + const brc20Routes = await getBRC20SupportedRoutes(ctx, toChain) + + return ( + brc20Routes.find( + route => + route.brc20Token === fromToken && + route.stacksToken === firstStepStacksToken, + ) != null && + brc20Routes.find( + route => + route.brc20Token === toToken && + route.stacksToken === finalStepStacksToken, + ) != null + ) + } + + // runes -> brc20 + if (KnownChainId.isRunesChain(fromChain)) { + if (!KnownTokenId.isRunesToken(fromToken)) return false + + const firstStepStacksToken = + route.swapRoute == null + ? await metaTokenToCorrespondingStacksToken(ctx, { + chain: fromChain, + token: fromToken, + }) + : await getFirstStepStacksTokenAddress(ctx, { + swap: route.swapRoute, + stacksChain: + fromChain === KnownChainId.Runes.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet, + }) + if (firstStepStacksToken == null) return false + + if (route.swapRoute == null) { + return firstStepStacksToken === finalStepStacksToken + } + + const fromRoutes = await getRunesSupportedRoutes(ctx, fromChain) + const toRoutes = await getBRC20SupportedRoutes(ctx, toChain) + + return ( + fromRoutes.find( + route => + route.runesToken === fromToken && + route.stacksToken === firstStepStacksToken, + ) != null && + toRoutes.find( + route => + route.brc20Token === toToken && + route.stacksToken === finalStepStacksToken, + ) != null + ) + } + + checkNever(fromChain) + return false } checkNever(toChain) return false } - -const getRoutesOverlapping = ( - brc20Routes: BRC20SupportedRoute[], - runesRoutes: RunesSupportedRoute[], -): null | [BRC20SupportedRoute, RunesSupportedRoute] => { - for (const brc20Route of brc20Routes) { - for (const runesRoute of runesRoutes) { - if (brc20Route.stacksToken === runesRoute.stacksToken) { - return [brc20Route, runesRoute] - } - } - } - return null -} diff --git a/src/metaUtils/tokenAddresses.ts b/src/metaUtils/tokenAddresses.ts new file mode 100644 index 0000000..965adf7 --- /dev/null +++ b/src/metaUtils/tokenAddresses.ts @@ -0,0 +1,59 @@ +import { KnownChainId, KnownTokenId } from "../utils/types/knownIds" +import { SDKGlobalContext } from "../xlinkSdkUtils/types.internal" +import { + getBRC20SupportedRoutes, + getRunesSupportedRoutes, +} from "./xlinkContractHelpers" + +export const brc20TokenFromTick = async ( + sdkContext: SDKGlobalContext, + chain: KnownChainId.BRC20Chain, + tick: string, +): Promise => { + const routes = await getBRC20SupportedRoutes(sdkContext, chain) + + const token = routes.find( + route => route.brc20Tick.toLowerCase() === tick.toLowerCase(), + )?.brc20Token + + if (token == null) return undefined + return { token } +} +export const brc20TokenToTick = async ( + sdkContext: SDKGlobalContext, + chain: KnownChainId.BRC20Chain, + token: KnownTokenId.BRC20Token, +): Promise => { + const routes = await getBRC20SupportedRoutes(sdkContext, chain) + const route = routes.find(route => route.brc20Token === token) + if (route == null) return undefined + return { tick: route.brc20Tick } +} + +export const runesTokenFromId = async ( + sdkContext: SDKGlobalContext, + chain: KnownChainId.RunesChain, + id: { blockHeight: bigint; txIndex: bigint }, +): Promise => { + const routes = await getRunesSupportedRoutes(sdkContext, chain) + + const idString = `${id.blockHeight}:${id.txIndex}` + const token = routes.find(route => route.runesId === idString)?.runesToken + + if (token == null) return undefined + return { token } +} +export const runesTokenToId = async ( + sdkContext: SDKGlobalContext, + chain: KnownChainId.RunesChain, + token: KnownTokenId.RunesToken, +): Promise => { + const routes = await getRunesSupportedRoutes(sdkContext, chain) + const route = routes.find(route => route.runesToken === token) + if (route == null) return undefined + + const id = route.runesId.split(":") as [`${number}`, `${number}`] + return { + id: { blockHeight: BigInt(id[0]), txIndex: BigInt(id[1]) }, + } +} diff --git a/src/metaUtils/xlinkContractHelpers.ts b/src/metaUtils/xlinkContractHelpers.ts index 91527b3..dc8b66d 100644 --- a/src/metaUtils/xlinkContractHelpers.ts +++ b/src/metaUtils/xlinkContractHelpers.ts @@ -43,7 +43,7 @@ export async function getBRC20SupportedRoutes( return promise } async function _getBRC20SupportedRoutes( - sdkContext: Pick, + sdkContext: SDKGlobalContext, chainId: KnownChainId.BRC20Chain, ): Promise { const stacksChainId = @@ -65,6 +65,7 @@ async function _getBRC20SupportedRoutes( const routes = await Promise.all( resp.routes.map(async (route): Promise => { const stacksToken = await getStacksToken( + sdkContext, stacksChainId, route.stacksTokenContractAddress, ) @@ -118,7 +119,6 @@ export interface RunesSupportedRoute { pegOutFeeRate: BigNumber pegOutFeeBitcoinAmount: null | BigNumber } - export async function getRunesSupportedRoutes( sdkContext: SDKGlobalContext, chainId: KnownChainId.RunesChain, @@ -141,7 +141,7 @@ export async function getRunesSupportedRoutes( return promise } async function _getRunesSupportedRoutes( - sdkContext: Pick, + sdkContext: SDKGlobalContext, chainId: KnownChainId.RunesChain, ): Promise { const stacksChainId = @@ -163,6 +163,7 @@ async function _getRunesSupportedRoutes( const routes = await Promise.all( resp.routes.map(async (route): Promise => { const stacksToken = await getStacksToken( + sdkContext, stacksChainId, route.stacksTokenContractAddress, ) diff --git a/src/stacksUtils/createBridgeOrder.ts b/src/stacksUtils/createBridgeOrder.ts deleted file mode 100644 index 3c46668..0000000 --- a/src/stacksUtils/createBridgeOrder.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { callReadOnlyFunction } from "@stacks/transactions" -import { CallReadOnlyFunctionFn, unwrapResponse } from "clarity-codegen" -import { toCorrespondingStacksToken } from "../evmUtils/peggingHelpers" -import { hasLength } from "../utils/arrayHelpers" -import { UnsupportedBridgeRouteError } from "../utils/errors" -import { decodeHex } from "../utils/hexHelpers" -import { checkNever } from "../utils/typeHelpers" -import { KnownChainId, KnownTokenId } from "../utils/types/knownIds" -import { StacksContractAddress } from "../xlinkSdkUtils/types" -import { contractAssignedChainIdFromKnownChain } from "./crossContractDataMapping" -import { getTerminatingStacksTokenContractAddress } from "./stxContractAddresses" -import { - addressToBuffer, - executeReadonlyCallXLINK, - getStacksContractCallInfo, - getStacksTokenContractInfo, -} from "./xlinkContractHelpers" - -export interface BridgeSwapRouteNode { - poolId: bigint - tokenContractAddress: `${string}.${string}::${string}` -} - -// prettier-ignore -export type BridgeSwapRoute_FromBitcoin = - | [] -// | [BridgeSwapRouteNode] -// | [BridgeSwapRouteNode, BridgeSwapRouteNode] -// | [BridgeSwapRouteNode, BridgeSwapRouteNode, BridgeSwapRouteNode] - -export interface CreateBridgeOrderResult { - terminatingStacksToken: StacksContractAddress - data: Uint8Array -} - -export async function createBridgeOrder_BitcoinToStacks(info: { - fromChain: KnownChainId.BitcoinChain - fromBitcoinScriptPubKey: Uint8Array - toChain: KnownChainId.StacksChain - toToken: KnownTokenId.StacksToken - toStacksAddress: string - swapRoute: BridgeSwapRoute_FromBitcoin - swapSlippedAmount?: bigint -}): Promise { - let data: undefined | Uint8Array - - const contractCallInfo = getStacksContractCallInfo( - info.toChain, - "btc-peg-in-endpoint-v2-05", - ) - if (contractCallInfo == null) { - throw new UnsupportedBridgeRouteError( - info.fromChain, - info.toChain, - KnownTokenId.Bitcoin.BTC, - info.toToken, - ) - } - - const { swapRoute, toStacksAddress /*, swapSlippedAmount = 0n */ } = info - const executeOptions = { - deployerAddress: contractCallInfo.deployerAddress, - callReadOnlyFunction: (callOptions => - callReadOnlyFunction({ - ...callOptions, - network: contractCallInfo.network, - })) satisfies CallReadOnlyFunctionFn, - } - - const targetTokenContractInfo = getStacksTokenContractInfo( - info.toChain, - info.toToken, - ) - if (targetTokenContractInfo == null) { - return undefined - } - - if (hasLength(swapRoute, 0)) { - data = await executeReadonlyCallXLINK( - contractCallInfo.contractName, - "create-order-cross-or-fail", - { - order: { - from: info.fromBitcoinScriptPubKey, - to: addressToBuffer(info.toChain, toStacksAddress), - "chain-id": undefined, - token: `${targetTokenContractInfo.deployerAddress}.${targetTokenContractInfo.contractName}`, - "token-out": `${targetTokenContractInfo.deployerAddress}.${targetTokenContractInfo.contractName}`, - }, - }, - executeOptions, - ).then(unwrapResponse) - } else { - checkNever(swapRoute) - } - - return { - terminatingStacksToken: targetTokenContractInfo, - data: data!, - } -} - -export async function createBridgeOrder_BitcoinToEVM(info: { - fromChain: KnownChainId.BitcoinChain - fromBitcoinScriptPubKey: Uint8Array - toChain: KnownChainId.EVMChain - toToken: KnownTokenId.EVMToken - toEVMAddress: string - swapRoute: BridgeSwapRoute_FromBitcoin - swapSlippedAmount?: bigint -}): Promise { - const contractCallInfo = getStacksContractCallInfo( - info.fromChain === KnownChainId.Bitcoin.Mainnet - ? KnownChainId.Stacks.Mainnet - : KnownChainId.Stacks.Testnet, - "btc-peg-in-endpoint-v2-05", - ) - if (contractCallInfo == null) { - throw new UnsupportedBridgeRouteError( - info.fromChain, - info.toChain, - KnownTokenId.Bitcoin.BTC, - info.toToken, - ) - } - - const { swapRoute, toEVMAddress /*, swapSlippedAmount = 0n */ } = info - const executeOptions = { - deployerAddress: contractCallInfo.deployerAddress, - callReadOnlyFunction: (callOptions => - callReadOnlyFunction({ - ...callOptions, - network: contractCallInfo.network, - })) satisfies CallReadOnlyFunctionFn, - } - - const targetChainId = contractAssignedChainIdFromKnownChain(info.toChain) - - const swappedStacksToken = await toCorrespondingStacksToken(info.toToken) - if (swappedStacksToken == null) return undefined - const swappedStacksTokenAddress = getStacksTokenContractInfo( - contractCallInfo.network.isMainnet() - ? KnownChainId.Stacks.Mainnet - : KnownChainId.Stacks.Testnet, - swappedStacksToken, - ) - if (swappedStacksTokenAddress == null) return undefined - - const terminatingStacksTokenAddress = - getTerminatingStacksTokenContractAddress({ - fromChain: contractCallInfo.network.isMainnet() - ? KnownChainId.Stacks.Mainnet - : KnownChainId.Stacks.Testnet, - fromToken: swappedStacksToken, - toChain: info.toChain, - toToken: info.toToken, - }) ?? swappedStacksTokenAddress - - let data: undefined | Uint8Array - if (hasLength(swapRoute, 0)) { - data = await executeReadonlyCallXLINK( - contractCallInfo.contractName, - "create-order-cross-or-fail", - { - order: { - from: info.fromBitcoinScriptPubKey, - to: decodeHex(toEVMAddress), - "chain-id": targetChainId, - token: `${swappedStacksTokenAddress.deployerAddress}.${swappedStacksTokenAddress.contractName}`, - "token-out": `${terminatingStacksTokenAddress.deployerAddress}.${terminatingStacksTokenAddress.contractName}`, - }, - }, - executeOptions, - ).then(unwrapResponse) - } else { - checkNever(swapRoute) - } - - return { - terminatingStacksToken: { - deployerAddress: terminatingStacksTokenAddress.deployerAddress, - contractName: terminatingStacksTokenAddress.contractName, - }, - data: data!, - } -} diff --git a/src/stacksUtils/createBridgeOrderFromBitcoin.ts b/src/stacksUtils/createBridgeOrderFromBitcoin.ts new file mode 100644 index 0000000..6b8f399 --- /dev/null +++ b/src/stacksUtils/createBridgeOrderFromBitcoin.ts @@ -0,0 +1,406 @@ +import { unwrapResponse } from "clarity-codegen" +import { evmTokenToCorrespondingStacksToken } from "../evmUtils/peggingHelpers" +import { + getBRC20SupportedRoutes, + getRunesSupportedRoutes, +} from "../metaUtils/xlinkContractHelpers" +import { + KnownRoute_FromBitcoin_ToBRC20, + KnownRoute_FromBitcoin_ToRunes, +} from "../utils/buildSupportedRoutes" +import { UnsupportedBridgeRouteError } from "../utils/errors" +import { decodeHex } from "../utils/hexHelpers" +import { SwapRoute_WithMinimumAmountsToReceive } from "../utils/SwapRouteHelpers" +import { assertExclude, checkNever } from "../utils/typeHelpers" +import { KnownChainId, KnownTokenId } from "../utils/types/knownIds" +import { StacksContractAddress } from "../xlinkSdkUtils/types" +import { SDKGlobalContext } from "../xlinkSdkUtils/types.internal" +import { contractAssignedChainIdFromKnownChain } from "./crossContractDataMapping" +import { + getTerminatingStacksTokenContractAddress, + StacksContractName, +} from "./stxContractAddresses" +import { + executeReadonlyCallXLINK, + getStacksContractCallInfo, + getStacksTokenContractInfo, + numberToStacksContractNumber, +} from "./xlinkContractHelpers" +import { addressToBuffer } from "../utils/addressHelpers" + +export interface BridgeSwapRouteNode { + poolId: bigint + tokenAddress: StacksContractAddress +} + +export interface CreateBridgeOrderResult { + terminatingStacksToken: StacksContractAddress + data: Uint8Array +} + +export async function createBridgeOrderFromBitcoin( + sdkContext: SDKGlobalContext, + info: { + fromChain: KnownChainId.BitcoinChain + fromBitcoinScriptPubKey: Uint8Array + toChain: + | KnownChainId.StacksChain + | KnownChainId.EVMChain + | KnownChainId.BRC20Chain + | KnownChainId.RunesChain + toToken: + | KnownTokenId.StacksToken + | KnownTokenId.EVMToken + | KnownTokenId.BRC20Token + | KnownTokenId.RunesToken + toAddress: string + toBitcoinScriptPubKey: Uint8Array + swap?: SwapRoute_WithMinimumAmountsToReceive + }, +): Promise { + if (KnownChainId.isStacksChain(info.toChain)) { + if (KnownTokenId.isStacksToken(info.toToken)) { + return createBridgeOrder_BitcoinToStacks(sdkContext, { + ...info, + toChain: info.toChain, + toToken: info.toToken, + toStacksAddress: info.toAddress, + }) + } + } + assertExclude(info.toChain, assertExclude.i()) + + if (KnownChainId.isEVMChain(info.toChain)) { + if (KnownTokenId.isEVMToken(info.toToken)) { + return createBridgeOrder_BitcoinToEVM(sdkContext, { + ...info, + toChain: info.toChain, + toToken: info.toToken, + toEVMAddress: info.toAddress, + }) + } + } + assertExclude(info.toChain, assertExclude.i()) + + if (KnownChainId.isBRC20Chain(info.toChain)) { + if (KnownTokenId.isBRC20Token(info.toToken)) { + return createBridgeOrder_BitcoinToMeta(sdkContext, { + ...info, + toChain: info.toChain, + toToken: info.toToken, + toBitcoinScriptPubKey: info.toBitcoinScriptPubKey, + }) + } + } + assertExclude(info.toChain, assertExclude.i()) + + if (KnownChainId.isRunesChain(info.toChain)) { + if (KnownTokenId.isRunesToken(info.toToken)) { + return createBridgeOrder_BitcoinToMeta(sdkContext, { + ...info, + toChain: info.toChain, + toToken: info.toToken, + toBitcoinScriptPubKey: info.toBitcoinScriptPubKey, + }) + } + } + assertExclude(info.toChain, assertExclude.i()) + + checkNever(info.toChain) + throw new UnsupportedBridgeRouteError( + info.fromChain, + info.toChain, + KnownTokenId.Bitcoin.BTC, + info.toToken, + ) +} + +export async function createBridgeOrder_BitcoinToStacks( + sdkContext: SDKGlobalContext, + info: { + fromChain: KnownChainId.BitcoinChain + fromBitcoinScriptPubKey: Uint8Array + toChain: KnownChainId.StacksChain + toToken: KnownTokenId.StacksToken + toStacksAddress: string + swap?: SwapRoute_WithMinimumAmountsToReceive + }, +): Promise { + let data: undefined | Uint8Array + + const contractBaseCallInfo = getStacksContractCallInfo( + info.toChain, + StacksContractName.BTCPegInEndpoint, + ) + const contractSwapCallInfo = getStacksContractCallInfo( + info.toChain, + StacksContractName.BTCPegInEndpointSwap, + ) + if (contractBaseCallInfo == null || contractSwapCallInfo == null) { + throw new UnsupportedBridgeRouteError( + info.fromChain, + info.toChain, + KnownTokenId.Bitcoin.BTC, + info.toToken, + ) + } + + const { toStacksAddress, swap: swapInfo } = info + + const targetTokenContractInfo = await getStacksTokenContractInfo( + sdkContext, + info.toChain, + info.toToken, + ) + if (targetTokenContractInfo == null) { + return undefined + } + + if (swapInfo == null) { + data = await executeReadonlyCallXLINK( + contractBaseCallInfo.contractName, + "create-order-cross-or-fail", + { + order: { + from: info.fromBitcoinScriptPubKey, + to: addressToBuffer(info.toChain, toStacksAddress), + "chain-id": undefined, + token: `${targetTokenContractInfo.deployerAddress}.${targetTokenContractInfo.contractName}`, + "token-out": `${targetTokenContractInfo.deployerAddress}.${targetTokenContractInfo.contractName}`, + }, + }, + contractBaseCallInfo.executeOptions, + ).then(unwrapResponse) + } else { + data = await executeReadonlyCallXLINK( + contractSwapCallInfo.contractName, + "create-order-cross-swap-or-fail", + { + order: { + from: info.fromBitcoinScriptPubKey, + to: addressToBuffer(info.toChain, toStacksAddress), + "chain-id": undefined, + routing: swapInfo.swapPools.map(n => n.poolId), + "min-amount-out": numberToStacksContractNumber( + swapInfo.minimumAmountsToReceive, + ), + "token-out": `${targetTokenContractInfo.deployerAddress}.${targetTokenContractInfo.contractName}`, + }, + }, + contractSwapCallInfo.executeOptions, + ).then(unwrapResponse) + } + + return { + terminatingStacksToken: targetTokenContractInfo, + data: data!, + } +} + +export async function createBridgeOrder_BitcoinToEVM( + sdkContext: SDKGlobalContext, + info: { + fromChain: KnownChainId.BitcoinChain + fromBitcoinScriptPubKey: Uint8Array + toChain: KnownChainId.EVMChain + toToken: KnownTokenId.EVMToken + toEVMAddress: string + swap?: SwapRoute_WithMinimumAmountsToReceive + }, +): Promise { + const contractBaseCallInfo = getStacksContractCallInfo( + info.fromChain === KnownChainId.Bitcoin.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet, + StacksContractName.BTCPegInEndpoint, + ) + const contractSwapCallInfo = getStacksContractCallInfo( + info.fromChain === KnownChainId.Bitcoin.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet, + StacksContractName.BTCPegInEndpointSwap, + ) + if (contractBaseCallInfo == null || contractSwapCallInfo == null) { + throw new UnsupportedBridgeRouteError( + info.fromChain, + info.toChain, + KnownTokenId.Bitcoin.BTC, + info.toToken, + ) + } + + const { swap: swapInfo, toEVMAddress } = info + + const targetChainId = contractAssignedChainIdFromKnownChain(info.toChain) + + const swappedStacksToken = await evmTokenToCorrespondingStacksToken( + info.toToken, + ) + if (swappedStacksToken == null) return undefined + const swappedStacksTokenAddress = await getStacksTokenContractInfo( + sdkContext, + contractBaseCallInfo.network.isMainnet() + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet, + swappedStacksToken, + ) + if (swappedStacksTokenAddress == null) return undefined + + const terminatingStacksTokenAddress = + getTerminatingStacksTokenContractAddress({ + fromChain: contractBaseCallInfo.network.isMainnet() + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet, + fromToken: swappedStacksToken, + toChain: info.toChain, + toToken: info.toToken, + }) ?? swappedStacksTokenAddress + + let data: undefined | Uint8Array + if (swapInfo == null) { + data = await executeReadonlyCallXLINK( + contractBaseCallInfo.contractName, + "create-order-cross-or-fail", + { + order: { + from: info.fromBitcoinScriptPubKey, + to: decodeHex(toEVMAddress), + "chain-id": targetChainId, + token: `${swappedStacksTokenAddress.deployerAddress}.${swappedStacksTokenAddress.contractName}`, + "token-out": `${terminatingStacksTokenAddress.deployerAddress}.${terminatingStacksTokenAddress.contractName}`, + }, + }, + contractBaseCallInfo.executeOptions, + ).then(unwrapResponse) + } else { + data = await executeReadonlyCallXLINK( + contractSwapCallInfo.contractName, + "create-order-cross-swap-or-fail", + { + order: { + from: info.fromBitcoinScriptPubKey, + to: decodeHex(toEVMAddress), + "chain-id": targetChainId, + routing: swapInfo.swapPools.map(n => n.poolId), + "min-amount-out": numberToStacksContractNumber( + swapInfo.minimumAmountsToReceive, + ), + "token-out": `${terminatingStacksTokenAddress.deployerAddress}.${terminatingStacksTokenAddress.contractName}`, + }, + }, + contractSwapCallInfo.executeOptions, + ).then(unwrapResponse) + } + + return { + terminatingStacksToken: { + deployerAddress: terminatingStacksTokenAddress.deployerAddress, + contractName: terminatingStacksTokenAddress.contractName, + }, + data: data!, + } +} + +export async function createBridgeOrder_BitcoinToMeta( + sdkContext: SDKGlobalContext, + info: Omit< + KnownRoute_FromBitcoin_ToBRC20 | KnownRoute_FromBitcoin_ToRunes, + "fromToken" + > & { + fromBitcoinScriptPubKey: Uint8Array + toBitcoinScriptPubKey: Uint8Array + swap?: SwapRoute_WithMinimumAmountsToReceive + }, +): Promise { + const contractBaseCallInfo = getStacksContractCallInfo( + info.fromChain === KnownChainId.Bitcoin.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet, + StacksContractName.BTCPegInEndpoint, + ) + const contractSwapCallInfo = getStacksContractCallInfo( + info.fromChain === KnownChainId.Bitcoin.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet, + StacksContractName.BTCPegInEndpointSwap, + ) + if (contractBaseCallInfo == null || contractSwapCallInfo == null) { + throw new UnsupportedBridgeRouteError( + info.fromChain, + info.toChain, + KnownTokenId.Bitcoin.BTC, + info.toToken, + ) + } + + const { swap: swapInfo } = info + + const targetChainId = contractAssignedChainIdFromKnownChain(info.toChain) + + // prettier-ignore + const swappedStacksToken = + KnownChainId.isBRC20Chain(info.toChain) ? + await getBRC20SupportedRoutes(sdkContext, info.toChain).then(routes => { + const route = routes.find(n => n.brc20Token === info.toToken) + return route?.stacksToken + }) : + KnownChainId.isRunesChain(info.toChain) ? + await getRunesSupportedRoutes(sdkContext, info.toChain).then(routes => { + const route = routes.find(n => n.runesToken === info.toToken) + return route?.stacksToken + }) : + (checkNever(info.toChain), undefined) + if (swappedStacksToken == null) return undefined + const swappedStacksTokenAddress = await getStacksTokenContractInfo( + sdkContext, + contractBaseCallInfo.network.isMainnet() + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet, + swappedStacksToken, + ) + if (swappedStacksTokenAddress == null) return undefined + + let data: undefined | Uint8Array + if (swapInfo == null) { + data = await executeReadonlyCallXLINK( + contractBaseCallInfo.contractName, + "create-order-cross-or-fail", + { + order: { + from: info.fromBitcoinScriptPubKey, + to: info.toBitcoinScriptPubKey, + "chain-id": targetChainId, + token: `${swappedStacksTokenAddress.deployerAddress}.${swappedStacksTokenAddress.contractName}`, + "token-out": `${swappedStacksTokenAddress.deployerAddress}.${swappedStacksTokenAddress.contractName}`, + }, + }, + contractBaseCallInfo.executeOptions, + ).then(unwrapResponse) + } else { + data = await executeReadonlyCallXLINK( + contractSwapCallInfo.contractName, + "create-order-cross-swap-or-fail", + { + order: { + from: info.fromBitcoinScriptPubKey, + to: info.toBitcoinScriptPubKey, + "chain-id": targetChainId, + routing: swapInfo.swapPools.map(n => n.poolId), + "min-amount-out": numberToStacksContractNumber( + swapInfo.minimumAmountsToReceive, + ), + "token-out": `${swappedStacksTokenAddress.deployerAddress}.${swappedStacksTokenAddress.contractName}`, + }, + }, + contractSwapCallInfo.executeOptions, + ).then(unwrapResponse) + } + + return { + terminatingStacksToken: { + deployerAddress: swappedStacksTokenAddress.deployerAddress, + contractName: swappedStacksTokenAddress.contractName, + }, + data: data!, + } +} diff --git a/src/stacksUtils/peggingHelpers.ts b/src/stacksUtils/peggingHelpers.ts index 4de9409..ea61af2 100644 --- a/src/stacksUtils/peggingHelpers.ts +++ b/src/stacksUtils/peggingHelpers.ts @@ -1,4 +1,4 @@ -import { fromCorrespondingStacksToken } from "../evmUtils/peggingHelpers" +import { evmTokenFromCorrespondingStacksToken } from "../evmUtils/peggingHelpers" import { getEVMTokenContractInfo } from "../evmUtils/xlinkContractHelpers" import { getBRC20SupportedRoutes, @@ -82,7 +82,10 @@ export const isSupportedStacksRoute: IsSupportedFn = async (ctx, route) => { const info = await getEVMTokenContractInfo(ctx, toChain, toToken) if (info == null) return false - const toEVMTokens = await fromCorrespondingStacksToken(toChain, fromToken) + const toEVMTokens = await evmTokenFromCorrespondingStacksToken( + toChain, + fromToken, + ) return toEVMTokens.includes(toToken) } diff --git a/src/stacksUtils/stxContractAddresses.ts b/src/stacksUtils/stxContractAddresses.ts index 7ce6c93..6f1e56f 100644 --- a/src/stacksUtils/stxContractAddresses.ts +++ b/src/stacksUtils/stxContractAddresses.ts @@ -1,3 +1,4 @@ +import { contractNameOverrides } from "../config" import { KnownRoute_FromStacks_ToEVM } from "../utils/buildSupportedRoutes" import { assertExclude, checkNever } from "../utils/typeHelpers" import { KnownChainId, KnownTokenId } from "../utils/types/knownIds" @@ -21,51 +22,281 @@ export const alexContractDeployerMainnet = export const alexContractDeployerTestnet = "ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK" +export const alexContractMultisigMainnet = + "SP1E0XBN9T4B10E9QMR7XMFJPMA19D77WY3KP2QKC" +export const alexContractMultisigTestnet = + "ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK" + export const legacyAlexContractDeployerMainnet = "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9" export const legacyAlexContractDeployerTestnet = "ST1J2JTYXGRMZYNKE40GM87ZCACSPSSEEQVSNB7DC" -const stxAlternativeTokenContractAddresses = { +const wrapContractAddress = ( + address: StacksContractAddress, +): StacksContractAddress => { + const contractName = + (contractNameOverrides as any)?.[address.contractName] ?? + address.contractName + return { + ...address, + contractName, + } +} + +export enum StacksContractName { + BTCPegInEndpoint = "btc-peg-in-endpoint-v2-05", + BTCPegInEndpointSwap = "btc-peg-in-endpoint-v2-07-swap", + BTCPegOutEndpoint = "btc-peg-out-endpoint-v2-01", + MetaPegInEndpoint = "meta-peg-in-endpoint-v2-04", + MetaPegInEndpointSwap = "meta-peg-in-endpoint-v2-06-swap", + MetaPegOutEndpoint = "meta-peg-out-endpoint-v2-04", + EVMPegInEndpoint = "cross-peg-in-endpoint-v2-04", + EVMPegOutEndpoint = "cross-peg-out-endpoint-v2-01", +} + +export const stxContractAddresses = { + [StacksContractName.BTCPegInEndpoint]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: xlinkContractsMultisigMainnet, + contractName: StacksContractName.BTCPegInEndpoint, + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: xlinkContractsMultisigTestnet, + contractName: StacksContractName.BTCPegInEndpoint, + }), + }, + [StacksContractName.BTCPegInEndpointSwap]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: xlinkContractsMultisigMainnet, + contractName: StacksContractName.BTCPegInEndpointSwap, + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: xlinkContractsMultisigTestnet, + contractName: StacksContractName.BTCPegInEndpointSwap, + }), + }, + [StacksContractName.BTCPegOutEndpoint]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: xlinkContractsDeployerMainnet, + contractName: StacksContractName.BTCPegOutEndpoint, + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: xlinkContractsDeployerTestnet, + contractName: StacksContractName.BTCPegOutEndpoint, + }), + }, + [StacksContractName.EVMPegInEndpoint]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: xlinkContractsMultisigMainnet, + contractName: StacksContractName.EVMPegInEndpoint, + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: xlinkContractsMultisigTestnet, + contractName: StacksContractName.EVMPegInEndpoint, + }), + }, + [StacksContractName.EVMPegOutEndpoint]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: xlinkContractsDeployerMainnet, + contractName: StacksContractName.EVMPegOutEndpoint, + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: xlinkContractsDeployerTestnet, + contractName: StacksContractName.EVMPegOutEndpoint, + }), + }, + [StacksContractName.MetaPegInEndpoint]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: xlinkContractsMultisigMainnet, + contractName: StacksContractName.MetaPegInEndpoint, + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: xlinkContractsMultisigTestnet, + contractName: StacksContractName.MetaPegInEndpoint, + }), + }, + [StacksContractName.MetaPegInEndpointSwap]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: xlinkContractsMultisigMainnet, + contractName: StacksContractName.MetaPegInEndpointSwap, + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: xlinkContractsMultisigTestnet, + contractName: StacksContractName.MetaPegInEndpointSwap, + }), + }, + [StacksContractName.MetaPegOutEndpoint]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: xlinkContractsMultisigMainnet, + contractName: StacksContractName.MetaPegOutEndpoint, + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: xlinkContractsMultisigTestnet, + contractName: StacksContractName.MetaPegOutEndpoint, + }), + }, +} satisfies Record< + StacksContractName, + Record +> + +export const stxTokenContractAddresses: Record< + string, + Record +> = { + [KnownTokenId.Stacks.ALEX]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: alexContractDeployerMainnet, + contractName: "token-alex", + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: alexContractDeployerTestnet, + contractName: "token-alex", + }), + }, + [KnownTokenId.Stacks.aBTC]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: xlinkContractsDeployerMainnet, + contractName: "token-abtc", + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: xlinkContractsDeployerTestnet, + contractName: "token-abtc", + }), + }, + [KnownTokenId.Stacks.sUSDT]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: xlinkContractsDeployerMainnet, + contractName: "token-susdt", + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: xlinkContractsDeployerTestnet, + contractName: "token-susdt", + }), + }, + [KnownTokenId.Stacks.sLUNR]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: xlinkContractsDeployerMainnet, + contractName: "token-slunr", + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: xlinkContractsDeployerTestnet, + contractName: "token-slunr", + }), + }, + [KnownTokenId.Stacks.sSKO]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: xlinkContractsDeployerMainnet, + contractName: "token-ssko", + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: xlinkContractsDeployerTestnet, + contractName: "token-ssko", + }), + }, + [KnownTokenId.Stacks.vLiSTX]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: alexContractDeployerMainnet, + contractName: "token-wvlqstx", + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: alexContractDeployerTestnet, + contractName: "token-wvlqstx", + }), + }, + [KnownTokenId.Stacks.vLiALEX]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: alexContractDeployerMainnet, + contractName: "token-wvlialex", + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: alexContractDeployerTestnet, + contractName: "token-wvlialex", + }), + }, + [KnownTokenId.Stacks.vLiaBTC]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: alexContractMultisigMainnet, + contractName: "token-wvliabtc", + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: alexContractDeployerTestnet, + contractName: "token-wvliabtc", + }), + }, + [KnownTokenId.Stacks.uBTC]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: xlinkContractsMultisigMainnet, + contractName: "token-ubtc", + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: xlinkContractsDeployerTestnet, + contractName: "token-ubtc", + }), + }, + [KnownTokenId.Stacks.DB20]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: legacyAlexContractDeployerMainnet, + contractName: "brc20-db20", + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: legacyAlexContractDeployerTestnet, + contractName: "brc20-db20", + }), + }, + [KnownTokenId.Stacks.DOG]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ + deployerAddress: legacyAlexContractDeployerMainnet, + contractName: "runes-dog", + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ + deployerAddress: legacyAlexContractDeployerTestnet, + contractName: "runes-dog", + }), + }, +} + +const terminatingStacksTokenContractAddresses = { wbtc: { - [KnownChainId.Stacks.Mainnet]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ deployerAddress: xlinkContractsMultisigMainnet, contractName: "token-wbtc", - }, - [KnownChainId.Stacks.Testnet]: { + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ deployerAddress: xlinkContractsDeployerTestnet, contractName: "token-wbtc", - }, + }), }, btcb: { - [KnownChainId.Stacks.Mainnet]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ deployerAddress: xlinkContractsMultisigMainnet, contractName: "token-wbtc", - }, - [KnownChainId.Stacks.Testnet]: { + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ deployerAddress: xlinkContractsDeployerTestnet, contractName: "token-btcb", - }, + }), }, cbBTC: { - [KnownChainId.Stacks.Mainnet]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ deployerAddress: xlinkContractsMultisigMainnet, contractName: "token-wbtc", - }, - [KnownChainId.Stacks.Testnet]: { + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ deployerAddress: xlinkContractsDeployerTestnet, contractName: "token-wbtc", - }, + }), }, usdt: { - [KnownChainId.Stacks.Mainnet]: { + [KnownChainId.Stacks.Mainnet]: wrapContractAddress({ deployerAddress: xlinkContractsMultisigMainnet, contractName: "token-usdt", - }, - [KnownChainId.Stacks.Testnet]: { + }), + [KnownChainId.Stacks.Testnet]: wrapContractAddress({ deployerAddress: xlinkContractsDeployerTestnet, contractName: "token-usdt", - }, + }), }, usdc: { [KnownChainId.Stacks.Mainnet]: { @@ -93,7 +324,7 @@ export const getTerminatingStacksTokenContractAddress = ( toChain === KnownChainId.EVM.Sepolia) && toToken === KnownTokenId.EVM.WBTC ) { - return stxAlternativeTokenContractAddresses.wbtc[route.fromChain] + return terminatingStacksTokenContractAddresses.wbtc[route.fromChain] } assertExclude(restChains, KnownChainId.EVM.Ethereum) assertExclude(restChains, KnownChainId.EVM.Sepolia) @@ -103,7 +334,7 @@ export const getTerminatingStacksTokenContractAddress = ( toChain === KnownChainId.EVM.BSCTestnet) && toToken === KnownTokenId.EVM.BTCB ) { - return stxAlternativeTokenContractAddresses.btcb[route.fromChain] + return terminatingStacksTokenContractAddresses.btcb[route.fromChain] } assertExclude(restChains, KnownChainId.EVM.BSC) assertExclude(restChains, KnownChainId.EVM.BSCTestnet) @@ -112,7 +343,7 @@ export const getTerminatingStacksTokenContractAddress = ( toChain === KnownChainId.EVM.Base && toToken === KnownTokenId.EVM.cbBTC ) { - return stxAlternativeTokenContractAddresses.cbBTC[route.fromChain] + return terminatingStacksTokenContractAddresses.cbBTC[route.fromChain] } assertExclude(restChains, KnownChainId.EVM.Base) @@ -129,7 +360,7 @@ export const getTerminatingStacksTokenContractAddress = ( toChain === KnownChainId.EVM.BSCTestnet) && toToken === KnownTokenId.EVM.USDT ) { - return stxAlternativeTokenContractAddresses.usdt[route.fromChain] + return terminatingStacksTokenContractAddresses.usdt[route.fromChain] } assertExclude(restChains, KnownChainId.EVM.Ethereum) assertExclude(restChains, KnownChainId.EVM.Sepolia) @@ -140,7 +371,7 @@ export const getTerminatingStacksTokenContractAddress = ( toChain === KnownChainId.EVM.Base && toToken === KnownTokenId.EVM.USDC ) { - return stxAlternativeTokenContractAddresses.usdt[route.fromChain] + return terminatingStacksTokenContractAddresses.usdc[route.fromChain] } assertExclude(restChains, KnownChainId.EVM.Base) @@ -161,7 +392,7 @@ export const getEVMTokenIdFromTerminatingStacksTokenContractAddress = (route: { route.evmChain === KnownChainId.EVM.Sepolia) && isStacksContractAddressEqual( route.stacksTokenAddress, - stxAlternativeTokenContractAddresses.wbtc[route.stacksChain], + terminatingStacksTokenContractAddresses.wbtc[route.stacksChain], ) ) { return KnownTokenId.EVM.WBTC @@ -174,7 +405,7 @@ export const getEVMTokenIdFromTerminatingStacksTokenContractAddress = (route: { route.evmChain === KnownChainId.EVM.BSCTestnet) && isStacksContractAddressEqual( route.stacksTokenAddress, - stxAlternativeTokenContractAddresses.btcb[route.stacksChain], + terminatingStacksTokenContractAddresses.btcb[route.stacksChain], ) ) { return KnownTokenId.EVM.BTCB @@ -186,7 +417,7 @@ export const getEVMTokenIdFromTerminatingStacksTokenContractAddress = (route: { route.evmChain === KnownChainId.EVM.Base && isStacksContractAddressEqual( route.stacksTokenAddress, - stxAlternativeTokenContractAddresses.cbBTC[route.stacksChain], + terminatingStacksTokenContractAddresses.cbBTC[route.stacksChain], ) ) { return KnownTokenId.EVM.cbBTC @@ -200,7 +431,7 @@ export const getEVMTokenIdFromTerminatingStacksTokenContractAddress = (route: { route.evmChain === KnownChainId.EVM.BSCTestnet) && isStacksContractAddressEqual( route.stacksTokenAddress, - stxAlternativeTokenContractAddresses.usdt[route.stacksChain], + terminatingStacksTokenContractAddresses.usdt[route.stacksChain], ) ) { return KnownTokenId.EVM.USDT @@ -214,7 +445,7 @@ export const getEVMTokenIdFromTerminatingStacksTokenContractAddress = (route: { route.evmChain === KnownChainId.EVM.Base && isStacksContractAddressEqual( route.stacksTokenAddress, - stxAlternativeTokenContractAddresses.usdc[route.stacksChain], + terminatingStacksTokenContractAddresses.usdc[route.stacksChain], ) ) { return KnownTokenId.EVM.USDC @@ -237,163 +468,3 @@ type ChainsHaveAlternativeBTC = | typeof KnownChainId.EVM.BSC | typeof KnownChainId.EVM.BSCTestnet | typeof KnownChainId.EVM.Base - -export const stxContractDeployers = { - "btc-peg-in-endpoint-v2-05": { - [KnownChainId.Stacks.Mainnet]: { - deployerAddress: xlinkContractsMultisigMainnet, - }, - [KnownChainId.Stacks.Testnet]: { - deployerAddress: xlinkContractsMultisigTestnet, - }, - }, - "btc-peg-out-endpoint-v2-01": { - [KnownChainId.Stacks.Mainnet]: { - deployerAddress: xlinkContractsDeployerMainnet, - }, - [KnownChainId.Stacks.Testnet]: { - deployerAddress: xlinkContractsDeployerTestnet, - }, - }, - "cross-peg-in-endpoint-v2-04": { - [KnownChainId.Stacks.Mainnet]: { - deployerAddress: xlinkContractsMultisigMainnet, - }, - [KnownChainId.Stacks.Testnet]: { - deployerAddress: xlinkContractsMultisigTestnet, - }, - }, - "cross-peg-out-endpoint-v2-01": { - [KnownChainId.Stacks.Mainnet]: { - deployerAddress: xlinkContractsDeployerMainnet, - }, - [KnownChainId.Stacks.Testnet]: { - deployerAddress: xlinkContractsDeployerTestnet, - }, - }, - "meta-peg-in-endpoint-v2-04": { - [KnownChainId.Stacks.Mainnet]: { - deployerAddress: xlinkContractsMultisigMainnet, - }, - [KnownChainId.Stacks.Testnet]: { - deployerAddress: xlinkContractsMultisigTestnet, - }, - }, - "meta-peg-out-endpoint-v2-04": { - [KnownChainId.Stacks.Mainnet]: { - deployerAddress: xlinkContractsMultisigMainnet, - }, - [KnownChainId.Stacks.Testnet]: { - deployerAddress: xlinkContractsMultisigTestnet, - }, - }, -} satisfies Record< - string, - Record -> - -export const stxTokenContractAddresses: Record< - KnownTokenId.StacksToken, - Record -> = { - [KnownTokenId.Stacks.ALEX]: { - [KnownChainId.Stacks.Mainnet]: { - deployerAddress: alexContractDeployerMainnet, - contractName: "token-alex", - }, - [KnownChainId.Stacks.Testnet]: { - deployerAddress: alexContractDeployerTestnet, - contractName: "token-alex", - }, - }, - [KnownTokenId.Stacks.aBTC]: { - [KnownChainId.Stacks.Mainnet]: { - deployerAddress: xlinkContractsDeployerMainnet, - contractName: "token-abtc", - }, - [KnownChainId.Stacks.Testnet]: { - deployerAddress: xlinkContractsDeployerTestnet, - contractName: "token-abtc", - }, - }, - [KnownTokenId.Stacks.sUSDT]: { - [KnownChainId.Stacks.Mainnet]: { - deployerAddress: xlinkContractsDeployerMainnet, - contractName: "token-susdt", - }, - [KnownChainId.Stacks.Testnet]: { - deployerAddress: xlinkContractsDeployerTestnet, - contractName: "token-susdt", - }, - }, - [KnownTokenId.Stacks.sLUNR]: { - [KnownChainId.Stacks.Mainnet]: { - deployerAddress: xlinkContractsDeployerMainnet, - contractName: "token-slunr", - }, - [KnownChainId.Stacks.Testnet]: { - deployerAddress: xlinkContractsDeployerTestnet, - contractName: "token-slunr", - }, - }, - [KnownTokenId.Stacks.sSKO]: { - [KnownChainId.Stacks.Mainnet]: { - deployerAddress: xlinkContractsDeployerMainnet, - contractName: "token-ssko", - }, - [KnownChainId.Stacks.Testnet]: { - deployerAddress: xlinkContractsDeployerTestnet, - contractName: "token-ssko", - }, - }, - [KnownTokenId.Stacks.vLiSTX]: { - [KnownChainId.Stacks.Mainnet]: { - deployerAddress: alexContractDeployerMainnet, - contractName: "token-wvlqstx", - }, - [KnownChainId.Stacks.Testnet]: { - deployerAddress: alexContractDeployerTestnet, - contractName: "token-wvlqstx", - }, - }, - [KnownTokenId.Stacks.vLiALEX]: { - [KnownChainId.Stacks.Mainnet]: { - deployerAddress: alexContractDeployerMainnet, - contractName: "token-wvlialex", - }, - [KnownChainId.Stacks.Testnet]: { - deployerAddress: alexContractDeployerTestnet, - contractName: "token-wvlialex", - }, - }, - [KnownTokenId.Stacks.uBTC]: { - [KnownChainId.Stacks.Mainnet]: { - deployerAddress: xlinkContractsMultisigMainnet, - contractName: "token-ubtc", - }, - [KnownChainId.Stacks.Testnet]: { - deployerAddress: xlinkContractsDeployerTestnet, - contractName: "token-ubtc", - }, - }, - [KnownTokenId.Stacks.DB20]: { - [KnownChainId.Stacks.Mainnet]: { - deployerAddress: legacyAlexContractDeployerMainnet, - contractName: "brc20-db20", - }, - [KnownChainId.Stacks.Testnet]: { - deployerAddress: legacyAlexContractDeployerTestnet, - contractName: "brc20-db20", - }, - }, - [KnownTokenId.Stacks.DOG]: { - [KnownChainId.Stacks.Mainnet]: { - deployerAddress: legacyAlexContractDeployerMainnet, - contractName: "runes-dog", - }, - [KnownChainId.Stacks.Testnet]: { - deployerAddress: legacyAlexContractDeployerTestnet, - contractName: "runes-dog", - }, - }, -} diff --git a/src/stacksUtils/validateBridgeOrder.ts b/src/stacksUtils/validateBridgeOrder.ts index f82cab5..199ea66 100644 --- a/src/stacksUtils/validateBridgeOrder.ts +++ b/src/stacksUtils/validateBridgeOrder.ts @@ -1,10 +1,9 @@ -import { callReadOnlyFunction } from "@stacks/transactions" -import { CallReadOnlyFunctionFn, Response } from "clarity-codegen" +import { Response } from "clarity-codegen" import { hasLength } from "../utils/arrayHelpers" -import { checkNever } from "../utils/typeHelpers" +import { SwapRoute } from "../utils/SwapRouteHelpers" import { KnownChainId } from "../utils/types/knownIds" import { StacksContractAddress } from "../xlinkSdkUtils/types" -import { BridgeSwapRoute_FromBitcoin } from "./createBridgeOrder" +import { StacksContractName } from "./stxContractAddresses" import { executeReadonlyCallXLINK, getStacksContractCallInfo, @@ -15,35 +14,33 @@ export async function validateBridgeOrder(info: { commitTx: Uint8Array revealTx: Uint8Array terminatingStacksToken: StacksContractAddress - swapRoute: BridgeSwapRoute_FromBitcoin + swapRoute?: SwapRoute }): Promise { - const contractCallInfo = getStacksContractCallInfo( + const contractBaseCallInfo = getStacksContractCallInfo( info.chainId === KnownChainId.Bitcoin.Mainnet ? KnownChainId.Stacks.Mainnet : KnownChainId.Stacks.Testnet, - "btc-peg-in-endpoint-v2-05", + StacksContractName.BTCPegInEndpoint, ) - if (contractCallInfo == null) { + const contractSwapCallInfo = getStacksContractCallInfo( + info.chainId === KnownChainId.Bitcoin.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet, + StacksContractName.BTCPegInEndpointSwap, + ) + if (contractBaseCallInfo == null || contractSwapCallInfo == null) { throw new Error( "[validateBridgeOrder_BitcoinToEVM] stacks contract information not found", ) } const { commitTx, revealTx, swapRoute } = info - const executeOptions = { - deployerAddress: contractCallInfo.deployerAddress, - callReadOnlyFunction: (callOptions => - callReadOnlyFunction({ - ...callOptions, - network: contractCallInfo.network, - })) satisfies CallReadOnlyFunctionFn, - } let resp: Response - if (hasLength(swapRoute, 0)) { + if (swapRoute == null || hasLength(swapRoute.swapPools, 0)) { resp = await executeReadonlyCallXLINK( - contractCallInfo.contractName, + contractBaseCallInfo.contractName, "validate-tx-cross", { "commit-tx": { @@ -56,10 +53,33 @@ export async function validateBridgeOrder(info: { }, "token-out-trait": `${info.terminatingStacksToken.deployerAddress}.${info.terminatingStacksToken.contractName}`, }, - executeOptions, + contractBaseCallInfo.executeOptions, + ) + } else if (swapRoute.swapPools.length < 4) { + resp = await executeReadonlyCallXLINK( + contractSwapCallInfo.contractName, + "validate-tx-cross-swap", + { + "commit-tx": { + tx: commitTx, + "output-idx": 1n, + }, + "reveal-tx": { + tx: revealTx, + "order-idx": 0n, + }, + "token-out-trait": `${info.terminatingStacksToken.deployerAddress}.${info.terminatingStacksToken.contractName}`, + "routing-traits": [ + `${swapRoute.fromTokenAddress.deployerAddress}.${swapRoute.fromTokenAddress.contractName}`, + ...swapRoute.swapPools.map( + (pool): `${string}.${string}` => + `${pool.toTokenAddress.deployerAddress}.${pool.toTokenAddress.contractName}`, + ), + ], + }, + contractSwapCallInfo.executeOptions, ) } else { - checkNever(swapRoute) throw new Error( `[validateBridgeOrder_BitcoinToEVM] unsupported swap route length: ${(swapRoute as any).length}`, ) diff --git a/src/stacksUtils/xlinkContractHelpers.ts b/src/stacksUtils/xlinkContractHelpers.ts index b3db244..0b14db4 100644 --- a/src/stacksUtils/xlinkContractHelpers.ts +++ b/src/stacksUtils/xlinkContractHelpers.ts @@ -1,32 +1,27 @@ -import { NETWORK, TEST_NETWORK } from "@scure/btc-signer" import { StacksNetwork } from "@stacks/network" +import { callReadOnlyFunction } from "@stacks/transactions" import { - c32address, - c32addressDecode, - versions as c32addressVersions, -} from "c32check" -import { + CallReadOnlyFunctionFn, composeTxOptionsFactory, executeReadonlyCallFactory, } from "clarity-codegen" import { xlinkContracts } from "../../generated/smartContract/contracts_xlink" -import { - addressToScriptPubKey, - scriptPubKeyToAddress, -} from "../bitcoinUtils/bitcoinHelpers" import { STACKS_MAINNET, STACKS_TESTNET } from "../config" +import { requestAPI } from "../utils/apiHelpers" import { BigNumber, BigNumberSource } from "../utils/BigNumber" -import { StacksAddressVersionNotSupportedError } from "../utils/errors" import { - decodeHex, - encodeHex, - encodeZeroPrefixedHex, -} from "../utils/hexHelpers" -import { checkNever } from "../utils/typeHelpers" -import { KnownChainId, KnownTokenId } from "../utils/types/knownIds" -import { StacksContractAddress } from "../xlinkSdkUtils/types" + createStacksToken, + KnownChainId, + KnownTokenId, +} from "../utils/types/knownIds" import { - stxContractDeployers, + isStacksContractAddressEqual, + StacksContractAddress, +} from "../xlinkSdkUtils/types" +import { SDKGlobalContext } from "../xlinkSdkUtils/types.internal" +import { + StacksContractName, + stxContractAddresses, stxTokenContractAddresses, } from "./stxContractAddresses" @@ -50,6 +45,16 @@ export const numberToStacksContractNumber = ( ) } +// const _composeTxXLINK = composeTxOptionsFactory(xlinkContracts, {}) +// export const composeTxXLINK: typeof _composeTxXLINK = (...args) => { +// const res = _composeTxXLINK(...args) +// return { +// ...res, +// contractName: +// (contractNameOverrides as any)?.[res.contractName] ?? res.contractName, +// } +// } + export const composeTxXLINK = composeTxOptionsFactory(xlinkContracts, {}) export const executeReadonlyCallXLINK = executeReadonlyCallFactory( @@ -57,9 +62,7 @@ export const executeReadonlyCallXLINK = executeReadonlyCallFactory( {}, ) -export const getStacksContractCallInfo = < - C extends keyof typeof stxContractDeployers, ->( +export const getStacksContractCallInfo = ( chainId: KnownChainId.StacksChain, contractName: C, ): @@ -67,39 +70,72 @@ export const getStacksContractCallInfo = < | (Omit & { contractName: C network: StacksNetwork + executeOptions: { + deployerAddress?: string + senderAddress?: string + callReadOnlyFunction?: CallReadOnlyFunctionFn + } }) => { const network = chainId === KnownChainId.Stacks.Mainnet ? STACKS_MAINNET : STACKS_TESTNET - if (stxContractDeployers[contractName][chainId] == null) { + if (stxContractAddresses[contractName][chainId] == null) { return undefined } return { - ...stxContractDeployers[contractName][chainId], + ...stxContractAddresses[contractName][chainId], contractName, network, + executeOptions: { + deployerAddress: + stxContractAddresses[contractName][chainId].deployerAddress, + callReadOnlyFunction(callOptions) { + return callReadOnlyFunction({ + ...callOptions, + contractAddress: + stxContractAddresses[contractName][chainId].deployerAddress, + contractName: + stxContractAddresses[contractName][chainId].contractName, + network, + }) + }, + }, } } -export const getStacksTokenContractInfo = ( +export const getStacksTokenContractInfo = async ( + ctx: SDKGlobalContext, chainId: KnownChainId.StacksChain, tokenId: KnownTokenId.StacksToken, -): undefined | (StacksContractAddress & { network: StacksNetwork }) => { - if (stxTokenContractAddresses[tokenId]?.[chainId] == null) { +): Promise< + undefined | (StacksContractAddress & { network: StacksNetwork }) +> => { + let address: StacksContractAddress | undefined + if (stxTokenContractAddresses[tokenId]?.[chainId] != null) { + address = stxTokenContractAddresses[tokenId][chainId] + } else { + const allTokens = await getAllStacksTokens(ctx, chainId) + for (const token of allTokens) { + if (token.stacksTokenId === tokenId) { + address = token.contractAddress + break + } + } + } + + if (address == null) { return undefined } const network = chainId === KnownChainId.Stacks.Mainnet ? STACKS_MAINNET : STACKS_TESTNET - return { - ...stxTokenContractAddresses[tokenId]![chainId], - network, - } + return { ...address, network } } export async function getStacksToken( + ctx: SDKGlobalContext, chain: KnownChainId.StacksChain, tokenAddress: StacksContractAddress, ): Promise { @@ -109,90 +145,87 @@ export async function getStacksToken( const info = stxTokenContractAddresses[token]?.[chain] if (info == null) continue - if ( - info.deployerAddress === tokenAddress.deployerAddress && - info.contractName === tokenAddress.contractName - ) { - return token + if (isStacksContractAddressEqual(info, tokenAddress)) { + return token as KnownTokenId.StacksToken + } + } + + const allTokens = await getAllStacksTokens(ctx, chain) + for (const token of allTokens) { + if (isStacksContractAddressEqual(token.contractAddress, tokenAddress)) { + return token.stacksTokenId + } + + if (token.underlyingToken != null) { + if ( + isStacksContractAddressEqual( + token.underlyingToken.contractAddress, + tokenAddress, + ) + ) { + return token.stacksTokenId + } } } return } -export function addressFromBuffer( - chain: KnownChainId.KnownChain, - buffer: Uint8Array, -): string { - if (KnownChainId.isStacksChain(chain)) { - return c32address( - c32addressVersions[ - chain === KnownChainId.Stacks.Mainnet ? "mainnet" : "testnet" - ].p2pkh, - encodeHex(buffer), - ) +const getAllStacksTokens = ( + ctx: SDKGlobalContext, + chain: KnownChainId.StacksChain, +): Promise => { + const cache = ctx.stacks.tokensCache + + if (cache == null) { + return getAllStacksTokensImpl(ctx, chain) } - if ( - KnownChainId.isBitcoinChain(chain) || - KnownChainId.isBRC20Chain(chain) || - KnownChainId.isRunesChain(chain) - ) { - return scriptPubKeyToAddress( - chain === KnownChainId.Bitcoin.Mainnet ? NETWORK : TEST_NETWORK, - buffer, - ) + const cached = cache.get(chain) + if (cached == null) { + const promise = getAllStacksTokensImpl(ctx, chain).catch(e => { + if (cache.get(chain) === promise) { + cache.delete(chain) + } + throw e + }) + cache.set(chain, promise) } - - if (KnownChainId.isEVMChain(chain)) { - return encodeZeroPrefixedHex(buffer) - } - - checkNever(chain) - throw new TypeError("[addressFromBuffer] Unsupported chain: " + chain) + return cache.get(chain)! } - -export function addressToBuffer( - chain: KnownChainId.KnownChain, - address: string, -): Uint8Array { - if (KnownChainId.isStacksChain(chain)) { - const [version, hash160] = c32addressDecode(address) - - if ( - (chain === KnownChainId.Stacks.Mainnet && - version == c32addressVersions.mainnet.p2sh) || - (chain === KnownChainId.Stacks.Testnet && - version == c32addressVersions.testnet.p2sh) - ) { - throw new StacksAddressVersionNotSupportedError(address, "Multisig") - } else if ( - (chain === KnownChainId.Stacks.Mainnet && - version !== c32addressVersions.mainnet.p2pkh) || - (chain === KnownChainId.Stacks.Testnet && - version !== c32addressVersions.testnet.p2pkh) - ) { - throw new StacksAddressVersionNotSupportedError(address, `${version}`) - } - - return decodeHex(hash160) - } - - if ( - KnownChainId.isBitcoinChain(chain) || - KnownChainId.isBRC20Chain(chain) || - KnownChainId.isRunesChain(chain) - ) { - return addressToScriptPubKey( - chain === KnownChainId.Bitcoin.Mainnet ? NETWORK : TEST_NETWORK, - address, - ) - } - - if (KnownChainId.isEVMChain(chain)) { - return decodeHex(address) - } - - checkNever(chain) - throw new TypeError("[addressToBuffer] Unsupported chain: " + chain) +const getAllStacksTokensImpl = async ( + ctx: SDKGlobalContext, + chain: KnownChainId.StacksChain, +): Promise => { + const res = await requestAPI<{ tokens: StacksTokenFromAPI[] }>(ctx, { + method: "GET", + path: "/2024-10-01/stacks/tokens", + query: { + network: chain === KnownChainId.Stacks.Mainnet ? "mainnet" : "testnet", + }, + }) + return res.tokens.map(info => ({ + stacksTokenId: createStacksToken(info.id), + contractAddress: info.contractAddress, + decimals: info.decimals, + underlyingToken: info.underlyingToken, + })) +} +export interface StacksTokenInfo { + stacksTokenId: KnownTokenId.StacksToken + contractAddress: StacksContractAddress + decimals: number + underlyingToken?: { + contractAddress: StacksContractAddress + decimals: number + } +} +interface StacksTokenFromAPI { + id: string + contractAddress: StacksContractAddress + decimals: number + underlyingToken?: { + contractAddress: StacksContractAddress + decimals: number + } } diff --git a/src/utils/Optional.ts b/src/utils/Optional.ts new file mode 100644 index 0000000..310d329 --- /dev/null +++ b/src/utils/Optional.ts @@ -0,0 +1,110 @@ +export type Optional = Optional.Some | Optional.None + +export namespace Optional { + export interface Some { + type: "some" + payload: T + } + + export interface None { + type: "none" + } + + export const isOptional = (input: any): input is Optional => { + if (input == null) return false + if (typeof input !== "object") return false + if (input.type !== "some" && input.type !== "none") return false + return true + } + + export const some = (payload: TOK): Optional.Some => ({ + type: "some", + payload, + }) + + export const none = (): Optional.None => ({ + type: "none", + }) + + export const maybeSome = (res: Optional): undefined | T => { + if (res.type === "some") return res.payload + return + } + + export function encaseOptional(fn: () => TOK): Optional { + try { + return some(fn()) + } catch (e: unknown) { + return none() + } + } + + export function encaseMaybeSome(fn: () => TOK): undefined | TOK + export function encaseMaybeSome( + fallbackValue: TFallback, + fn: () => TOK, + ): TOK | TFallback + export function encaseMaybeSome( + ...args: [fallbackValue: TFallback, fn: () => TOK] | [fn: () => TOK] + ): undefined | TOK | TFallback { + const [fallbackValue, fn] = args.length === 1 ? [undefined, args[0]] : args + const res = encaseOptional(fn) + return res.type === "some" ? res.payload : fallbackValue + } + + interface CurriedMapFn { + ( + mapping: (payload: TOKInput) => TOKOutput, + ): (res: Optional) => Optional + ( + mapping: (payload: TOKInput) => TOKOutput, + res: Optional, + ): Optional + } + export const map: CurriedMapFn = ( + mapping: (payload: TOKInput) => TOKOutput, + res?: Optional, + ): any => { + if (res == null) { + return (res: Optional) => realMap(mapping, res) + } else { + return realMap(mapping, res) + } + + function realMap( + mapping: (payload: TOKInput) => TOKOutput, + res: Optional, + ): Optional { + if (res.type === "none") return res + return Optional.some(mapping(res.payload)) + } + } + + interface CurriedFlatMapFn { + ( + mapping: (payload: TOKInput) => Optional, + ): (res: Optional) => Optional + ( + mapping: (payload: TOKInput) => Optional, + res: Optional, + ): Optional + } + export const flatMap: CurriedFlatMapFn = ( + mapping: (payload: TOKInput) => Optional, + res?: Optional, + ): any => { + if (res == null) { + return (res: Optional) => realFlatMap(mapping, res) + } else { + return realFlatMap(mapping, res) + } + + function realFlatMap( + mapping: (payload: TOKInput) => Optional, + res: Optional, + ): Optional { + if (res.type === "none") return res + return mapping(res.payload) + } + } +} diff --git a/src/utils/Result.ts b/src/utils/Result.ts new file mode 100644 index 0000000..4543295 --- /dev/null +++ b/src/utils/Result.ts @@ -0,0 +1,164 @@ +export type Result = Result.OK | Result.Error + +export namespace Result { + export interface OK { + type: "ok" + payload: T + } + + export interface Error { + type: "error" + payload: T + } + + export const isResult = ( + input: any, + ): input is Result => { + if (input == null) return false + if (typeof input !== "object") return false + if (input.type !== "ok" && input.type !== "error") return false + if (!("payload" in input)) return false + return true + } + + export const ok = (payload: TOK): Result.OK => ({ + type: "ok", + payload, + }) + + export const error = ( + payload: TError, + ): Result.Error => ({ + type: "error", + payload, + }) + + export const maybeValue = ( + res: Result, + ): undefined | TOK => { + if (res.type === "ok") return res.payload + return + } + + export const maybeError = ( + res: Result, + ): undefined | TError => { + if (res.type === "error") return res.payload + return + } + + export const encase = ( + fn: () => TOK, + ): Result => { + try { + return ok(fn()) + } catch (e: unknown) { + return error(e) as any + } + } + export function encaseMaybeValue(fn: () => TOK): undefined | TOK + export function encaseMaybeValue( + fallbackValue: TFallback, + fn: () => TOK, + ): TOK | TFallback + export function encaseMaybeValue( + ...args: [fallbackValue: TFallback, fn: () => TOK] | [fn: () => TOK] + ): undefined | TOK | TFallback { + const [fallbackValue, fn] = args.length === 1 ? [undefined, args[0]] : args + const res = encase(fn) + return res.type === "ok" ? res.payload : fallbackValue + } + + interface CurriedMapFn { + ( + mapping: (payload: TOKInput) => TOKOutput, + ): (res: Result) => Result + ( + mapping: (payload: TOKInput) => TOKOutput, + res: Result, + ): Result + } + export const map: CurriedMapFn = ( + mapping: (payload: TOKInput) => TOKOutput, + res?: Result, + ): any => { + if (res == null) { + return (res: Result) => realMap(mapping, res) + } else { + return realMap(mapping, res) + } + + function realMap( + mapping: (payload: TOKInput) => TOKOutput, + res: Result, + ): Result { + if (res.type === "error") return res + return Result.ok(mapping(res.payload)) + } + } + + interface CurriedChainErrorFn { + ( + mapping: (payload: TErrorInput) => Result, + ): ( + res: Result, + ) => Result + ( + mapping: (payload: TErrorInput) => Result, + res: Result, + ): Result + } + export const chainError: CurriedChainErrorFn = < + TOKInput, + TErrorInput, + TOKOutput, + TErrorOutput, + >( + chainFn: (payload: TErrorInput) => Result, + res?: Result, + ): any => { + if (res == null) { + return (res: Result) => + realChainError(chainFn, res) + } else { + return realChainError(chainFn, res) + } + + function realChainError( + chainFn: (payload: TErrorInput) => Result, + res: Result, + ): Result { + if (res.type !== "error") return res + return chainFn(res.payload) + } + } + + interface CurriedFlatMapFn { + ( + mapping: (payload: TOKInput) => Result, + ): (res: Result) => Result + ( + mapping: (payload: TOKInput) => Result, + res: Result, + ): Result + } + export const flatMap: CurriedFlatMapFn = ( + mapping: (payload: TOKInput) => Result, + res?: Result, + ): any => { + if (res == null) { + return (res: Result) => realMap(mapping, res) + } else { + return realMap(mapping, res) + } + + function realMap( + mapping: (payload: TOKInput) => Result, + res: Result, + ): Result { + if (res.type === "error") return res + return mapping(res.payload) + } + } + export const chainOk = flatMap +} diff --git a/src/utils/ResultHelpers.ts b/src/utils/ResultHelpers.ts new file mode 100644 index 0000000..e0ecea4 --- /dev/null +++ b/src/utils/ResultHelpers.ts @@ -0,0 +1,16 @@ +import { identity } from "./funcHelpers" +import { Result } from "./Result" + +export function encase( + mapping: (res: T) => T1, + fn: () => T, +): undefined | T1 +export function encase(fn: () => T): undefined | T +export function encase( + ...args: [mapping: (res: T) => T1, fn: () => T] | [fn: () => T] +): undefined | T1 { + const [mapping, fn] = args.length === 1 ? [undefined, args[0]] : args + return Result.maybeValue( + Result.map(mapping ?? (identity as any), Result.encase(fn)), + ) +} diff --git a/src/utils/SwapRouteHelpers.ts b/src/utils/SwapRouteHelpers.ts new file mode 100644 index 0000000..9a79170 --- /dev/null +++ b/src/utils/SwapRouteHelpers.ts @@ -0,0 +1,164 @@ +import { evmTokenToCorrespondingStacksToken } from "../evmUtils/peggingHelpers" +import { metaTokenToCorrespondingStacksToken } from "../metaUtils/peggingHelpers" +import { getStacksToken } from "../stacksUtils/xlinkContractHelpers" +import { SDKNumber, StacksContractAddress } from "../xlinkSdkUtils/types" +import { SDKGlobalContext } from "../xlinkSdkUtils/types.internal" +import { last } from "./arrayHelpers" +import { BigNumber } from "./BigNumber" +import { KnownRoute_WithMetaProtocol } from "./buildSupportedRoutes" +import { UnsupportedBridgeRouteError } from "./errors" +import { checkNever, OneOrMore } from "./typeHelpers" +import { KnownChainId, KnownTokenId } from "./types/knownIds" + +export interface SwapRoute { + fromTokenAddress: StacksContractAddress + swapPools: OneOrMore<{ + poolId: bigint + toTokenAddress: StacksContractAddress + }> +} + +export interface SwapRoute_WithExchangeRate extends SwapRoute { + composedExchangeRate: BigNumber +} +export interface SwapRoute_WithExchangeRate_Public extends SwapRoute { + composedExchangeRate: SDKNumber +} + +export interface SwapRoute_WithMinimumAmountsToReceive extends SwapRoute { + minimumAmountsToReceive: BigNumber +} +export interface SwapRoute_WithMinimumAmountsToReceive_Public + extends SwapRoute { + minimumAmountsToReceive: SDKNumber +} + +export async function getFirstStepStacksTokenAddress( + sdkContext: SDKGlobalContext, + info: { + swap: SwapRoute + stacksChain: KnownChainId.StacksChain + }, +): Promise { + return getStacksToken( + sdkContext, + info.stacksChain, + info.swap.fromTokenAddress, + ) +} + +export async function getFinalStepStacksTokenAddress( + sdkContext: SDKGlobalContext, + info: { + swap: SwapRoute + stacksChain: KnownChainId.StacksChain + }, +): Promise { + const finalStepStacksTokenAddress = last(info.swap.swapPools).toTokenAddress + + return getStacksToken( + sdkContext, + info.stacksChain, + finalStepStacksTokenAddress, + ) +} + +export async function getTransitStacksChainTransitStepInfos( + ctx: SDKGlobalContext, + info: KnownRoute_WithMetaProtocol & { + swapRoute?: SwapRoute_WithExchangeRate_Public + }, +): Promise<{ + step1ToStacksToken: KnownTokenId.StacksToken + step2FromStacksToken: KnownTokenId.StacksToken +}> { + const transitStacksChainId = + info.fromChain === KnownChainId.Bitcoin.Mainnet || + info.fromChain === KnownChainId.BRC20.Mainnet || + info.fromChain === KnownChainId.Runes.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet + + const [ + step1ToStacksToken, + step2FromStacksToken, + swapStartToken, + swapEndToken, + ] = await Promise.all([ + toCorrespondingStacksToken(info.fromChain, info.fromToken), + toCorrespondingStacksToken(info.toChain, info.toToken), + info.swapRoute == null + ? null + : getFirstStepStacksTokenAddress(ctx, { + swap: info.swapRoute, + stacksChain: transitStacksChainId, + }), + info.swapRoute == null + ? null + : getFinalStepStacksTokenAddress(ctx, { + swap: info.swapRoute, + stacksChain: transitStacksChainId, + }), + ]) + + if ( + step1ToStacksToken == null || + step2FromStacksToken == null || + (info.swapRoute != null && + (swapStartToken !== step1ToStacksToken || + swapEndToken !== step2FromStacksToken)) + ) { + throw new UnsupportedBridgeRouteError( + info.fromChain, + info.toChain, + info.fromToken, + info.toToken, + ) + } + + return { + step1ToStacksToken, + step2FromStacksToken, + } + + async function toCorrespondingStacksToken( + chain: KnownChainId.KnownChain, + token: KnownTokenId.KnownToken, + ): Promise { + let toStacksTokenPromise: + | undefined + | Promise + + if (KnownChainId.isBitcoinChain(chain)) { + if (token === KnownTokenId.Bitcoin.BTC) { + toStacksTokenPromise = Promise.resolve(KnownTokenId.Stacks.aBTC) + } + } else if (KnownChainId.isBRC20Chain(chain)) { + if (KnownTokenId.isBRC20Token(token)) { + toStacksTokenPromise = metaTokenToCorrespondingStacksToken(ctx, { + chain: chain as KnownChainId.BRC20Chain, + token: token as KnownTokenId.BRC20Token, + }) + } + } else if (KnownChainId.isRunesChain(chain)) { + if (KnownTokenId.isRunesToken(token)) { + toStacksTokenPromise = metaTokenToCorrespondingStacksToken(ctx, { + chain: chain as KnownChainId.RunesChain, + token: token as KnownTokenId.RunesToken, + }) + } + } else if (KnownChainId.isEVMChain(chain)) { + if (KnownTokenId.isEVMToken(token)) { + toStacksTokenPromise = evmTokenToCorrespondingStacksToken(token) + } + } else if (KnownChainId.isStacksChain(chain)) { + if (KnownTokenId.isStacksToken(token)) { + toStacksTokenPromise = Promise.resolve(token) + } + } else { + checkNever(chain) + } + + return toStacksTokenPromise + } +} diff --git a/src/utils/addressHelpers.ts b/src/utils/addressHelpers.ts new file mode 100644 index 0000000..89c441a --- /dev/null +++ b/src/utils/addressHelpers.ts @@ -0,0 +1,105 @@ +import { NETWORK, TEST_NETWORK } from "@scure/btc-signer" +import { + c32address, + c32addressDecode, + versions as c32addressVersions, +} from "c32check" +import { + addressToScriptPubKey, + scriptPubKeyToAddress, +} from "../bitcoinUtils/bitcoinHelpers" +import { StacksAddressVersionNotSupportedError } from "./errors" +import { decodeHex, encodeHex, encodeZeroPrefixedHex } from "./hexHelpers" +import { checkNever } from "./typeHelpers" +import { KnownChainId } from "./types/knownIds" + +export function addressFromBuffer( + chain: KnownChainId.KnownChain, + buffer: Uint8Array, +): string { + if (KnownChainId.isStacksChain(chain)) { + return c32address( + c32addressVersions[ + chain === KnownChainId.Stacks.Mainnet ? "mainnet" : "testnet" + ].p2pkh, + encodeHex(buffer), + ) + } + + if ( + KnownChainId.isBitcoinChain(chain) || + KnownChainId.isBRC20Chain(chain) || + KnownChainId.isRunesChain(chain) + ) { + const network = + chain === KnownChainId.Bitcoin.Mainnet || + chain === KnownChainId.BRC20.Mainnet || + chain === KnownChainId.Runes.Mainnet + ? NETWORK + : chain === KnownChainId.Bitcoin.Testnet || + chain === KnownChainId.BRC20.Testnet || + chain === KnownChainId.Runes.Testnet + ? TEST_NETWORK + : (checkNever(chain), NETWORK) + return scriptPubKeyToAddress(network, buffer) + } + + if (KnownChainId.isEVMChain(chain)) { + return encodeZeroPrefixedHex(buffer) + } + + checkNever(chain) + throw new TypeError("[addressFromBuffer] Unsupported chain: " + chain) +} + +export function addressToBuffer( + chain: KnownChainId.KnownChain, + address: string, +): Uint8Array { + if (KnownChainId.isStacksChain(chain)) { + const [version, hash160] = c32addressDecode(address) + + if ( + (chain === KnownChainId.Stacks.Mainnet && + version == c32addressVersions.mainnet.p2sh) || + (chain === KnownChainId.Stacks.Testnet && + version == c32addressVersions.testnet.p2sh) + ) { + throw new StacksAddressVersionNotSupportedError(address, "Multisig") + } else if ( + (chain === KnownChainId.Stacks.Mainnet && + version !== c32addressVersions.mainnet.p2pkh) || + (chain === KnownChainId.Stacks.Testnet && + version !== c32addressVersions.testnet.p2pkh) + ) { + throw new StacksAddressVersionNotSupportedError(address, `${version}`) + } + + return decodeHex(hash160) + } + + if ( + KnownChainId.isBitcoinChain(chain) || + KnownChainId.isBRC20Chain(chain) || + KnownChainId.isRunesChain(chain) + ) { + const network = + chain === KnownChainId.Bitcoin.Mainnet || + chain === KnownChainId.BRC20.Mainnet || + chain === KnownChainId.Runes.Mainnet + ? NETWORK + : chain === KnownChainId.Bitcoin.Testnet || + chain === KnownChainId.BRC20.Testnet || + chain === KnownChainId.Runes.Testnet + ? TEST_NETWORK + : (checkNever(chain), NETWORK) + return addressToScriptPubKey(network, address) + } + + if (KnownChainId.isEVMChain(chain)) { + return decodeHex(address) + } + + checkNever(chain) + throw new TypeError("[addressToBuffer] Unsupported chain: " + chain) +} diff --git a/src/utils/buildSupportedRoutes.ts b/src/utils/buildSupportedRoutes.ts index cbc5bf7..1a61d22 100644 --- a/src/utils/buildSupportedRoutes.ts +++ b/src/utils/buildSupportedRoutes.ts @@ -1,4 +1,5 @@ import { ChainId, TokenId } from "../xlinkSdkUtils/types" +import { SwapRoute } from "./SwapRouteHelpers" import { SDKGlobalContext } from "../xlinkSdkUtils/types.internal" import { UnsupportedBridgeRouteError } from "./errors" import { pMemoize } from "./pMemoize" @@ -53,23 +54,23 @@ export type KnownRoute_FromBitcoin_ToEVM = { toChain: KnownChainId.EVMChain toToken: KnownTokenId.EVMToken } -// export type KnownRoute_FromBitcoin_ToBRC20 = { -// fromChain: KnownChainId.BitcoinChain -// fromToken: KnownTokenId.BitcoinToken -// toChain: KnownChainId.BRC20Chain -// toToken: KnownTokenId.BRC20Token -// } -// export type KnownRoute_FromBitcoin_ToRunes = { -// fromChain: KnownChainId.BitcoinChain -// fromToken: KnownTokenId.BitcoinToken -// toChain: KnownChainId.RunesChain -// toToken: KnownTokenId.RunesToken -// } +export type KnownRoute_FromBitcoin_ToBRC20 = { + fromChain: KnownChainId.BitcoinChain + fromToken: KnownTokenId.BitcoinToken + toChain: KnownChainId.BRC20Chain + toToken: KnownTokenId.BRC20Token +} +export type KnownRoute_FromBitcoin_ToRunes = { + fromChain: KnownChainId.BitcoinChain + fromToken: KnownTokenId.BitcoinToken + toChain: KnownChainId.RunesChain + toToken: KnownTokenId.RunesToken +} export type KnownRoute_FromBitcoin = | KnownRoute_FromBitcoin_ToStacks | KnownRoute_FromBitcoin_ToEVM -// | KnownRoute_FromBitcoin_ToBRC20 -// | KnownRoute_FromBitcoin_ToRunes + | KnownRoute_FromBitcoin_ToBRC20 + | KnownRoute_FromBitcoin_ToRunes export type KnownRoute_FromEVM_ToStacks = { fromChain: KnownChainId.EVMChain @@ -108,24 +109,98 @@ export type KnownRoute_FromEVM = | KnownRoute_FromEVM_ToRunes | KnownRoute_FromEVM_ToEVM -export type _KnownRoute_FromBRC20_ToStacks = { +export type KnownRoute_FromBRC20_ToStacks = { fromChain: KnownChainId.BRC20Chain fromToken: KnownTokenId.BRC20Token toChain: KnownChainId.StacksChain toToken: KnownTokenId.StacksToken } -export type _KnownRoute_FromRunes_ToStacks = { +export type KnownRoute_FromBRC20_ToEVM = { + fromChain: KnownChainId.BRC20Chain + fromToken: KnownTokenId.BRC20Token + toChain: KnownChainId.EVMChain + toToken: KnownTokenId.EVMToken +} +export type KnownRoute_FromBRC20_ToBitcoin = { + fromChain: KnownChainId.BRC20Chain + fromToken: KnownTokenId.BRC20Token + toChain: KnownChainId.BitcoinChain + toToken: KnownTokenId.BitcoinToken +} +export type KnownRoute_FromBRC20_ToBRC20 = { + fromChain: KnownChainId.BRC20Chain + fromToken: KnownTokenId.BRC20Token + toChain: KnownChainId.BRC20Chain + toToken: KnownTokenId.BRC20Token +} +export type KnownRoute_FromBRC20_ToRunes = { + fromChain: KnownChainId.BRC20Chain + fromToken: KnownTokenId.BRC20Token + toChain: KnownChainId.RunesChain + toToken: KnownTokenId.RunesToken +} +export type _KnownRoute_FromBRC20 = + | KnownRoute_FromBRC20_ToStacks + | KnownRoute_FromBRC20_ToEVM + | KnownRoute_FromBRC20_ToBitcoin + | KnownRoute_FromBRC20_ToBRC20 + | KnownRoute_FromBRC20_ToRunes + +export type KnownRoute_FromRunes_ToStacks = { fromChain: KnownChainId.RunesChain fromToken: KnownTokenId.RunesToken toChain: KnownChainId.StacksChain toToken: KnownTokenId.StacksToken } +export type KnownRoute_FromRunes_ToEVM = { + fromChain: KnownChainId.RunesChain + fromToken: KnownTokenId.RunesToken + toChain: KnownChainId.EVMChain + toToken: KnownTokenId.EVMToken +} +export type KnownRoute_FromRunes_ToBitcoin = { + fromChain: KnownChainId.RunesChain + fromToken: KnownTokenId.RunesToken + toChain: KnownChainId.BitcoinChain + toToken: KnownTokenId.BitcoinToken +} +export type KnownRoute_FromRunes_ToBRC20 = { + fromChain: KnownChainId.RunesChain + fromToken: KnownTokenId.RunesToken + toChain: KnownChainId.BRC20Chain + toToken: KnownTokenId.BRC20Token +} +export type KnownRoute_FromRunes_ToRunes = { + fromChain: KnownChainId.RunesChain + fromToken: KnownTokenId.RunesToken + toChain: KnownChainId.RunesChain + toToken: KnownTokenId.RunesToken +} +export type _KnownRoute_FromRunes = + | KnownRoute_FromRunes_ToStacks + | KnownRoute_FromRunes_ToEVM + | KnownRoute_FromRunes_ToBitcoin + | KnownRoute_FromRunes_ToBRC20 + | KnownRoute_FromRunes_ToRunes export type KnownRoute = | KnownRoute_FromStacks | KnownRoute_FromBitcoin | KnownRoute_FromEVM +export type KnownRoute_WithMetaProtocol = + | KnownRoute_FromStacks + | KnownRoute_FromEVM + | KnownRoute_FromBitcoin + | _KnownRoute_FromBRC20 + | _KnownRoute_FromRunes + +export type KnownRoute_ToStacks = + | KnownRoute_FromBitcoin_ToStacks + | KnownRoute_FromEVM_ToStacks + | KnownRoute_FromBRC20_ToStacks + | KnownRoute_FromRunes_ToStacks + export function defineRoute( chainPairs: [fromChains: ChainId[], toChains: ChainId[]], tokenPairs: [fromToken: TokenId, toToken: TokenId][], @@ -150,7 +225,9 @@ export function defineRoute( export type IsSupportedFn = ( ctx: SDKGlobalContext, - route: DefinedRoute, + route: DefinedRoute & { + swapRoute?: SwapRoute + }, ) => Promise const memoizedIsSupportedFactory = ( isSupported: IsSupportedFn, @@ -158,7 +235,12 @@ const memoizedIsSupportedFactory = ( return pMemoize( { cacheKey([, route]) { - return `${route.fromChain}:${route.fromToken}->${route.toChain}:${route.toToken}` + const from = `${route.fromChain}:${route.fromToken}` + const to = `${route.toChain}:${route.toToken}` + if (route.swapRoute == null) return `${from}->${to}` + + const swap = route.swapRoute.swapPools.map(p => p.poolId).join("->") + return `${from}->(${swap})->${to}` }, skipCache: true, }, @@ -168,8 +250,8 @@ const memoizedIsSupportedFactory = ( export interface GetSupportedRoutesFn_Conditions { fromChain?: ChainId - toChain?: ChainId fromToken?: TokenId + toChain?: ChainId toToken?: TokenId } export type GetSupportedRoutesFn = ( @@ -179,7 +261,9 @@ export type GetSupportedRoutesFn = ( export type CheckRouteValidFn = ( ctx: SDKGlobalContext, - route: DefinedRoute, + route: DefinedRoute & { + swapRoute?: SwapRoute + }, ) => Promise export function buildSupportedRoutes( diff --git a/src/utils/feeRateHelpers.ts b/src/utils/feeRateHelpers.ts index 76e0fd8..7fb9795 100644 --- a/src/utils/feeRateHelpers.ts +++ b/src/utils/feeRateHelpers.ts @@ -19,12 +19,34 @@ export interface TransferProphetAppliedResult { export const applyTransferProphets = ( transferProphets: OneOrMore, amount: BigNumber, + options: { + exchangeRates?: readonly BigNumber[] + } = {}, ): OneOrMore => { + const { exchangeRates } = options + + if ( + exchangeRates != null && + exchangeRates.length < transferProphets.length - 1 + ) { + throw new Error( + `[XLinkSDK#applyTransferProphets] exchangeRate count not match with transferProphet count, which is not expected`, + ) + } + return reduce( - (acc, transferProphet) => - concat(acc, [applyTransferProphet(transferProphet, last(acc).netAmount)]), - [{ fees: [], netAmount: amount }], - transferProphets, + (acc, transferProphet, idx) => + concat(acc, [ + applyTransferProphet( + transferProphet, + BigNumber.mul( + last(acc).netAmount, + exchangeRates?.[idx] ?? BigNumber.ONE, + ), + ), + ]), + [applyTransferProphet(transferProphets[0], amount)], + transferProphets.slice(1), ) } @@ -70,6 +92,9 @@ export const applyTransferProphet = ( } /** + * @deprecated + * **DO NOT** use this function before we created it's unit tests + * * @example * composeTransferProphets( * [ @@ -92,6 +117,15 @@ export const composeTransferProphets = ( transferProphets: readonly TransferProphet[], exchangeRates: readonly BigNumber[], ): TransferProphetAggregated => { + if ( + exchangeRates != null && + exchangeRates.length < transferProphets.length - 1 + ) { + throw new Error( + `[XLinkSDK#composeTransferProphets] exchangeRate count not match with transferProphet count, which is not expected`, + ) + } + const cumulativeExchangeRates = calcCumulativeExchangeRates(exchangeRates) return reduce( @@ -99,7 +133,7 @@ export const composeTransferProphets = ( ...composeTransferProphet2( res, transferProphet, - cumulativeExchangeRates[idx], + cumulativeExchangeRates[idx + 1], ), transferProphets: [...res.transferProphets, transferProphet], }), @@ -114,18 +148,18 @@ export const composeTransferProphets = ( /** * @example * calcCumulativeExchangeRates([ - * exchangeRateOf(tokenA, tokenB), - * exchangeRateOf(tokenB, tokenC), - * exchangeRateOf(tokenC, tokenD), + * exchangeRateOf(A, B), + * exchangeRateOf(B, C), + * exchangeRateOf(C, D), * // ... * ]) * * // => * * [ - * exchangeRateOf(tokenA, tokenB), - * exchangeRateOf(tokenA, tokenC), - * exchangeRateOf(tokenA, tokenD), + * exchangeRateOf(A, B), + * exchangeRateOf(A, C), + * exchangeRateOf(A, D), * // ... * ] */ @@ -140,30 +174,6 @@ export const composeTransferProphet2 = ( transferProphet2: TransferProphet, exchangeRate: BigNumber, ): TransferProphetAggregated<[TransferProphet, TransferProphet]> => { - if (exchangeRate == null) { - throw new Error( - "[XLinkSDK#composeTransferProphet2] exchangeRate is required", - ) - } - - const bridgeTokenMinFeeAmount = BigNumber.sum([ - ...transferProphet1.fees.flatMap(f => - f.type === "rate" && f.token === transferProphet1.bridgeToken - ? [f.minimumAmount] - : [], - ), - ...transferProphet2.fees.flatMap(f => - f.type === "rate" && f.token === transferProphet2.bridgeToken - ? [ - /** - * convert to the denomination of the first step token - */ - BigNumber.div(f.minimumAmount, exchangeRate), - ] - : [], - ), - ]) - /** * flatFeeRate = 1 - (amount1 * (1-feeRate1) * (1-feeRate2) * (1-feeRateN...) / amount1) * flatFeeRate = 1 - (1-feeRate1) * (1-feeRate2) * (1-feeRateN...) @@ -181,15 +191,36 @@ export const composeTransferProphet2 = ( ), ), ) + /** * step2Amount = step1Amount * (1-feeRate1) * (1-feeRate2) * (1-feeRateN)... * exchangeRate - * step2Amount = step1Amount * (1-flatFeeRate) * exchangeRate + * step2Amount = step1Amount * (1-flatFeeRate1) * exchangeRate + * step2Amount = step1Amount * step1ToStep2Rate + * step1ToStep2Rate = (1-flatFeeRate1) * exchangeRate */ const step1ToStep2Rate = BigNumber.mul( BigNumber.minus(1, step1FlatFeeRate), exchangeRate, ) + const bridgeTokenMinFeeAmount = BigNumber.sum([ + ...transferProphet1.fees.flatMap(f => + f.type === "rate" && f.token === transferProphet1.bridgeToken + ? [f.minimumAmount] + : [], + ), + ...transferProphet2.fees.flatMap(f => + f.type === "rate" && f.token === transferProphet2.bridgeToken + ? [ + /** + * convert to the denomination of the first step token + */ + BigNumber.div(f.minimumAmount, step1ToStep2Rate), + ] + : [], + ), + ]) + let minBridgeAmount: BigNumber | null = null if ( BigNumber.isZero(bridgeTokenMinFeeAmount) /* min fee amount not set */ && diff --git a/src/utils/funcHelpers.ts b/src/utils/funcHelpers.ts new file mode 100644 index 0000000..ec18a45 --- /dev/null +++ b/src/utils/funcHelpers.ts @@ -0,0 +1 @@ +export const identity = (x: T): T => x diff --git a/src/utils/objectHelper.ts b/src/utils/objectHelper.ts new file mode 100644 index 0000000..4c1a463 --- /dev/null +++ b/src/utils/objectHelper.ts @@ -0,0 +1,6 @@ +export function entries(obj: T): [keyof T, T[keyof T]][] { + return Object.entries(obj as any) as [keyof T, T[keyof T]][] +} +export function fromEntries(entries: [keyof T, T[keyof T]][]): T { + return Object.fromEntries(entries) as T +} diff --git a/src/utils/types/TransferProphet.ts b/src/utils/types/TransferProphet.ts index 73b4c1b..4147250 100644 --- a/src/utils/types/TransferProphet.ts +++ b/src/utils/types/TransferProphet.ts @@ -4,12 +4,12 @@ import { toSDKNumberOrUndefined, } from "../../xlinkSdkUtils/types" import { BigNumber } from "../BigNumber" -import { last } from "../arrayHelpers" -import { KnownRoute } from "../buildSupportedRoutes" +import { first, last } from "../arrayHelpers" +import { KnownRoute_WithMetaProtocol } from "../buildSupportedRoutes" import { applyTransferProphet, applyTransferProphets, - composeTransferProphets, + composeTransferProphet2, } from "../feeRateHelpers" import { checkNever, isNotNull, OneOrMore } from "../typeHelpers" import { KnownChainId, KnownTokenId } from "./knownIds" @@ -98,7 +98,7 @@ export function transformFromPublicTransferProphet( } } export function transformToPublicTransferProphet( - route: KnownRoute, + route: KnownRoute_WithMetaProtocol, fromAmount: SDKNumber | BigNumber, transferProphet: TransferProphet, ): PublicTransferProphet { @@ -155,20 +155,28 @@ export function transformToPublicTransferProphet( * ], * ) */ -export const transformToPublicTransferProphetAggregated = ( - transferProphets: OneOrMore, - exchangeRates: readonly BigNumber[], -): PublicTransferProphetAggregated> => { - const firstTransferProphet = transferProphets[0] - const lastTransferProphet = last(transferProphets) +export const transformToPublicTransferProphetAggregated2 = ( + routes: [KnownRoute_WithMetaProtocol, KnownRoute_WithMetaProtocol], + transferProphets: [TransferProphet, TransferProphet], + fromAmount: BigNumber, + exchangeRate: BigNumber, +): PublicTransferProphetAggregated< + [PublicTransferProphet, PublicTransferProphet] +> => { + if (routes.length !== transferProphets.length) { + throw new Error( + `[XLinkSDK#transformToPublicTransferProphetAggregated2] route count not match with transferProphet count, which is not expected`, + ) + } - const steps = transferProphets.map( - transformFromPublicTransferProphet, - ) as any as OneOrMore - const composed = composeTransferProphets(steps, exchangeRates) - - const fromAmount = BigNumber.from(firstTransferProphet.fromAmount) - const applyResult = applyTransferProphets(steps, fromAmount) + const composed = composeTransferProphet2( + transferProphets[0], + transferProphets[1], + exchangeRate, + ) + const applyResult = applyTransferProphets(transferProphets, fromAmount, { + exchangeRates: [exchangeRate], + }) const fees: PublicTransferProphet["fees"] = applyResult .flatMap(r => r.fees) @@ -189,17 +197,30 @@ export const transformToPublicTransferProphetAggregated = ( ) .filter(isNotNull) + const firstRoute = first(routes) + const lastRoute = last(routes) return { - fromChain: firstTransferProphet.fromChain, - fromToken: firstTransferProphet.fromToken, - toChain: lastTransferProphet.toChain, - toToken: lastTransferProphet.toToken, + fromChain: firstRoute.fromChain, + fromToken: firstRoute.fromToken, + toChain: lastRoute.toChain, + toToken: lastRoute.toToken, fromAmount: toSDKNumberOrUndefined(fromAmount), toAmount: toSDKNumberOrUndefined(last(applyResult).netAmount), isPaused: composed.isPaused, fees, minBridgeAmount: toSDKNumberOrUndefined(composed.minBridgeAmount), maxBridgeAmount: toSDKNumberOrUndefined(composed.maxBridgeAmount), - transferProphets, + transferProphets: [ + transformToPublicTransferProphet( + routes[0], + fromAmount, + transferProphets[0], + ), + transformToPublicTransferProphet( + routes[1], + applyResult[1].netAmount, + transferProphets[1], + ), + ], } } diff --git a/src/utils/types/knownIds.ts b/src/utils/types/knownIds.ts index 893fc21..02832a1 100644 --- a/src/utils/types/knownIds.ts +++ b/src/utils/types/knownIds.ts @@ -104,16 +104,21 @@ export namespace KnownTokenId { export const vLiSTX = tokenId("stx-vlistx") /** Represents the vLiALEX token ID on the Stacks blockchain. */ export const vLiALEX = tokenId("stx-vlialex") + export const vLiaBTC = tokenId("stx-vliabtc") export const uBTC = tokenId("stx-ubtc") export const DB20 = tokenId("stx-db20") export const DOG = tokenId("stx-dog") } - /** This type includes all known tokens on the Stacks blockchain. */ - export type StacksToken = (typeof _allKnownStacksTokens)[number] + export type StacksToken = TokenId<`stx-${string}`> export function isStacksToken(value: TokenId): value is StacksToken { - return _allKnownStacksTokens.includes(value as any) + return value.startsWith("stx-") } } +export const createStacksToken = ( + stacksTokenId: string, +): KnownTokenId.StacksToken => { + return `stx-${stacksTokenId}` as any +} export const createBRC20Token = ( brc20tick: string, ): KnownTokenId.BRC20Token => { @@ -126,7 +131,6 @@ export const createRunesToken = ( } export const _allKnownBitcoinTokens = Object.values(KnownTokenId.Bitcoin) export const _allKnownEVMTokens = Object.values(KnownTokenId.EVM) -export const _allKnownStacksTokens = Object.values(KnownTokenId.Stacks) /** * The `KnownChainId` namespace provides types of blockchain networks diff --git a/src/xlinkSdkUtils/bridgeFromBitcoin.ts b/src/xlinkSdkUtils/bridgeFromBitcoin.ts index 8557cd1..a3933af 100644 --- a/src/xlinkSdkUtils/bridgeFromBitcoin.ts +++ b/src/xlinkSdkUtils/bridgeFromBitcoin.ts @@ -12,27 +12,27 @@ import { prepareTransaction, } from "../bitcoinUtils/prepareTransaction" import { - BridgeSwapRoute_FromBitcoin, CreateBridgeOrderResult, createBridgeOrder_BitcoinToEVM, + createBridgeOrder_BitcoinToMeta, createBridgeOrder_BitcoinToStacks, -} from "../stacksUtils/createBridgeOrder" +} from "../stacksUtils/createBridgeOrderFromBitcoin" import { validateBridgeOrder } from "../stacksUtils/validateBridgeOrder" -import { - getStacksTokenContractInfo, - numberToStacksContractNumber, -} from "../stacksUtils/xlinkContractHelpers" +import { getStacksTokenContractInfo } from "../stacksUtils/xlinkContractHelpers" import { range } from "../utils/arrayHelpers" import { BigNumber } from "../utils/BigNumber" import { KnownRoute_FromBitcoin, + KnownRoute_FromBitcoin_ToBRC20, KnownRoute_FromBitcoin_ToEVM, + KnownRoute_FromBitcoin_ToRunes, KnownRoute_FromBitcoin_ToStacks, buildSupportedRoutes, defineRoute, } from "../utils/buildSupportedRoutes" import { BridgeValidateFailedError, + InvalidMethodParametersError, UnsupportedBridgeRouteError, } from "../utils/errors" import { decodeHex } from "../utils/hexHelpers" @@ -42,8 +42,11 @@ import { KnownTokenId, _allKnownEVMMainnetChains, _allKnownEVMTestnetChains, + _knownChainIdToErrorMessagePart, } from "../utils/types/knownIds" import { ChainId, SDKNumber, TokenId } from "./types" +import { SwapRoute_WithMinimumAmountsToReceive_Public } from "../utils/SwapRouteHelpers" +import { SwapRoute } from "../utils/SwapRouteHelpers" import { SDKGlobalContext } from "./types.internal" export const supportedRoutes = buildSupportedRoutes( @@ -98,8 +101,13 @@ export interface BridgeFromBitcoinInput { fromAddress: string fromAddressScriptPubKey: Uint8Array toAddress: string + /** + * **Required** when `toChain` is one of bitcoin chains + */ + toAddressScriptPubKey?: Uint8Array amount: SDKNumber networkFeeRate: bigint + swapRoute?: SwapRoute_WithMinimumAmountsToReceive_Public reselectSpendableUTXOs: ReselectSpendableUTXOsFn signPsbt: BridgeFromBitcoinInput_signPsbtFn sendTransaction: (tx: { hex: string }) => Promise<{ @@ -144,13 +152,32 @@ export async function bridgeFromBitcoin( toToken: route.toToken, }) } - } else if ( - KnownChainId.isBRC20Chain(route.toChain) || - KnownChainId.isRunesChain(route.toChain) - ) { - assertExclude(route.toChain, assertExclude.i()) - assertExclude(route.toChain, assertExclude.i()) - // TODO: bitcoin to brc20/runes is not supported yet + } else if (KnownChainId.isBRC20Chain(route.toChain)) { + if ( + KnownTokenId.isBitcoinToken(route.fromToken) && + KnownTokenId.isBRC20Token(route.toToken) + ) { + return bridgeFromBitcoin_toMeta(ctx, { + ...info, + fromChain: route.fromChain, + toChain: route.toChain, + fromToken: route.fromToken, + toToken: route.toToken, + }) + } + } else if (KnownChainId.isRunesChain(route.toChain)) { + if ( + KnownTokenId.isBitcoinToken(route.fromToken) && + KnownTokenId.isRunesToken(route.toToken) + ) { + return bridgeFromBitcoin_toMeta(ctx, { + ...info, + fromChain: route.fromChain, + toChain: route.toChain, + fromToken: route.fromToken, + toToken: route.toToken, + }) + } } else { assertExclude(route.toChain, assertExclude.i()) checkNever(route) @@ -170,7 +197,7 @@ export async function bridgeFromBitcoin( } async function bridgeFromBitcoin_toStacks( - sdkContext: Pick, + sdkContext: SDKGlobalContext, info: Omit< BridgeFromBitcoinInput, "fromChain" | "toChain" | "fromToken" | "toToken" @@ -178,7 +205,8 @@ async function bridgeFromBitcoin_toStacks( KnownRoute_FromBitcoin_ToStacks, ): Promise { const pegInAddress = getBTCPegInAddress(info.fromChain, info.toChain) - const toTokenContractInfo = getStacksTokenContractInfo( + const toTokenContractInfo = await getStacksTokenContractInfo( + sdkContext, info.toChain, info.toToken, ) @@ -190,14 +218,21 @@ async function bridgeFromBitcoin_toStacks( ) } - const createdOrder = await createBridgeOrder_BitcoinToStacks({ + const createdOrder = await createBridgeOrder_BitcoinToStacks(sdkContext, { fromChain: info.fromChain, fromBitcoinScriptPubKey: info.fromAddressScriptPubKey, toChain: info.toChain, toToken: info.toToken, toStacksAddress: info.toAddress, - swapSlippedAmount: numberToStacksContractNumber(info.amount), - swapRoute: [], + swap: + info.swapRoute == null + ? undefined + : { + ...info.swapRoute, + minimumAmountsToReceive: BigNumber.from( + info.swapRoute.minimumAmountsToReceive, + ), + }, }) if (createdOrder == null) { throw new UnsupportedBridgeRouteError( @@ -211,21 +246,77 @@ async function bridgeFromBitcoin_toStacks( } async function bridgeFromBitcoin_toEVM( - sdkContext: Pick, + sdkContext: SDKGlobalContext, info: Omit< BridgeFromBitcoinInput, "fromChain" | "toChain" | "fromToken" | "toToken" > & KnownRoute_FromBitcoin_ToEVM, ): Promise { - const createdOrder = await createBridgeOrder_BitcoinToEVM({ + const createdOrder = await createBridgeOrder_BitcoinToEVM(sdkContext, { fromChain: info.fromChain, toChain: info.toChain, toToken: info.toToken, fromBitcoinScriptPubKey: info.fromAddressScriptPubKey, toEVMAddress: info.toAddress, - swapSlippedAmount: numberToStacksContractNumber(info.amount), - swapRoute: [], + swap: + info.swapRoute == null + ? undefined + : { + ...info.swapRoute, + minimumAmountsToReceive: BigNumber.from( + info.swapRoute.minimumAmountsToReceive, + ), + }, + }) + if (createdOrder == null) { + throw new UnsupportedBridgeRouteError( + info.fromChain, + info.toChain, + KnownTokenId.Bitcoin.BTC, + ) + } + + return broadcastBitcoinTransaction(sdkContext, info, createdOrder) +} + +async function bridgeFromBitcoin_toMeta( + sdkContext: SDKGlobalContext, + info: Omit< + BridgeFromBitcoinInput, + "fromChain" | "toChain" | "fromToken" | "toToken" + > & + (KnownRoute_FromBitcoin_ToBRC20 | KnownRoute_FromBitcoin_ToRunes), +): Promise { + if (info.toAddressScriptPubKey == null) { + throw new InvalidMethodParametersError( + [ + "XLinkSDK", + `bridgeFromBitcoin (to ${_knownChainIdToErrorMessagePart(info.toChain)})`, + ], + [ + { + name: "toAddressScriptPubKey", + expected: "Uint8Array", + received: "undefined", + }, + ], + ) + } + + const createdOrder = await createBridgeOrder_BitcoinToMeta(sdkContext, { + ...info, + fromBitcoinScriptPubKey: info.fromAddressScriptPubKey, + toBitcoinScriptPubKey: info.toAddressScriptPubKey, + swap: + info.swapRoute == null + ? undefined + : { + ...info.swapRoute, + minimumAmountsToReceive: BigNumber.from( + info.swapRoute.minimumAmountsToReceive, + ), + }, }) if (createdOrder == null) { throw new UnsupportedBridgeRouteError( @@ -239,7 +330,7 @@ async function bridgeFromBitcoin_toEVM( } async function broadcastBitcoinTransaction( - sdkContext: Pick, + sdkContext: SDKGlobalContext, info: Omit< ConstructBitcoinTransactionInput, "validateBridgeOrder" | "orderData" | "pegInAddress" @@ -324,11 +415,11 @@ type ConstructBitcoinTransactionInput = PrepareBitcoinTransactionInput & { validateBridgeOrder: ( pegInTx: Uint8Array, revealTx: undefined | Uint8Array, - swapRoute: BridgeSwapRoute_FromBitcoin, + swapRoute?: SwapRoute, ) => Promise } async function constructBitcoinTransaction( - sdkContext: Pick, + sdkContext: SDKGlobalContext, info: ConstructBitcoinTransactionInput, ): Promise<{ hex: string @@ -375,9 +466,16 @@ async function constructBitcoinTransaction( } await info - .validateBridgeOrder(signedTx.extract(), revealTx, []) + .validateBridgeOrder(signedTx.extract(), revealTx, info.swapRoute) .catch(err => { - throw new BridgeValidateFailedError(err) + if (sdkContext.btc.ignoreValidateResult) { + console.error( + "Bridge tx validation failed, but ignoreValidateResult is true, so we ignore the error", + err, + ) + } else { + throw new BridgeValidateFailedError(err) + } }) return { @@ -391,6 +489,7 @@ export type PrepareBitcoinTransactionInput = KnownRoute_FromBitcoin & { fromAddress: BridgeFromBitcoinInput["fromAddress"] toAddress: BridgeFromBitcoinInput["toAddress"] amount: BridgeFromBitcoinInput["amount"] + swapRoute?: BridgeFromBitcoinInput["swapRoute"] networkFeeRate: BridgeFromBitcoinInput["networkFeeRate"] reselectSpendableUTXOs: BridgeFromBitcoinInput["reselectSpendableUTXOs"] orderData: Uint8Array diff --git a/src/xlinkSdkUtils/bridgeFromEVM.ts b/src/xlinkSdkUtils/bridgeFromEVM.ts index c5be8aa..1fdee34 100644 --- a/src/xlinkSdkUtils/bridgeFromEVM.ts +++ b/src/xlinkSdkUtils/bridgeFromEVM.ts @@ -11,10 +11,8 @@ import { numberToSolidityContractNumber, } from "../evmUtils/xlinkContractHelpers" import { contractAssignedChainIdFromKnownChain } from "../stacksUtils/crossContractDataMapping" -import { - addressToBuffer, - getStacksTokenContractInfo, -} from "../stacksUtils/xlinkContractHelpers" +import { getStacksTokenContractInfo } from "../stacksUtils/xlinkContractHelpers" +import { addressToBuffer } from "../utils/addressHelpers" import { BigNumber } from "../utils/BigNumber" import { buildSupportedRoutes, @@ -350,6 +348,7 @@ async function bridgeFromEVM_toStacks( info.fromToken, ) const toTokenContractInfo = await getStacksTokenContractInfo( + ctx, info.toChain, info.toToken, ) @@ -639,7 +638,7 @@ async function bridgeFromEVM_toMeta( throw new InvalidMethodParametersError( [ "XLinkSDK", - `bridgeFromEVM (to ${KnownChainId.isBRC20Chain(info.toChain) ? "BRC20" : "Runes"})`, + `bridgeFromEVM (to ${_knownChainIdToErrorMessagePart(info.toChain)})`, ], [ { diff --git a/src/xlinkSdkUtils/bridgeFromStacks.ts b/src/xlinkSdkUtils/bridgeFromStacks.ts index e76180e..3d3408e 100644 --- a/src/xlinkSdkUtils/bridgeFromStacks.ts +++ b/src/xlinkSdkUtils/bridgeFromStacks.ts @@ -3,6 +3,10 @@ import { ContractCallOptions } from "clarity-codegen" import { addressToScriptPubKey } from "../bitcoinUtils/bitcoinHelpers" import { contractAssignedChainIdFromKnownChain } from "../stacksUtils/crossContractDataMapping" import { isSupportedStacksRoute } from "../stacksUtils/peggingHelpers" +import { + getTerminatingStacksTokenContractAddress, + StacksContractName, +} from "../stacksUtils/stxContractAddresses" import { composeTxXLINK, getStacksContractCallInfo, @@ -21,14 +25,13 @@ import { UnsupportedBridgeRouteError } from "../utils/errors" import { decodeHex } from "../utils/hexHelpers" import { assertExclude, checkNever } from "../utils/typeHelpers" import { - KnownChainId, - KnownTokenId, _allKnownEVMMainnetChains, _allKnownEVMTestnetChains, + KnownChainId, + KnownTokenId, } from "../utils/types/knownIds" import { ChainId, SDKNumber, TokenId } from "./types" import { SDKGlobalContext } from "./types.internal" -import { getTerminatingStacksTokenContractAddress } from "../stacksUtils/stxContractAddresses" export const supportedRoutes = buildSupportedRoutes( [ @@ -139,7 +142,7 @@ export async function bridgeFromStacks( KnownTokenId.isStacksToken(route.fromToken) && KnownTokenId.isEVMToken(route.toToken) ) { - return bridgeFromStacks_toEVM({ + return bridgeFromStacks_toEVM(ctx, { ...info, fromChain: route.fromChain, toChain: route.toChain, @@ -152,7 +155,7 @@ export async function bridgeFromStacks( KnownTokenId.isStacksToken(route.fromToken) && KnownTokenId.isBRC20Token(route.toToken) ) { - return bridgeFromStacks_toMeta({ + return bridgeFromStacks_toMeta(ctx, { ...info, fromChain: route.fromChain, toChain: route.toChain, @@ -165,7 +168,7 @@ export async function bridgeFromStacks( KnownTokenId.isStacksToken(route.fromToken) && KnownTokenId.isRunesToken(route.toToken) ) { - return bridgeFromStacks_toMeta({ + return bridgeFromStacks_toMeta(ctx, { ...info, fromChain: route.fromChain, toChain: route.toChain, @@ -200,7 +203,7 @@ async function bridgeFromStacks_toBitcoin( ): Promise { const contractCallInfo = getStacksContractCallInfo( info.fromChain, - "btc-peg-out-endpoint-v2-01", + StacksContractName.BTCPegOutEndpoint, ) if (!contractCallInfo) { throw new UnsupportedBridgeRouteError( @@ -223,13 +226,14 @@ async function bridgeFromStacks_toBitcoin( "peg-out-address": addressToScriptPubKey(bitcoinNetwork, info.toAddress), amount: numberToStacksContractNumber(info.amount), }, - { deployerAddress: contractCallInfo.deployerAddress }, + contractCallInfo.executeOptions, ) return await info.sendTransaction(options) } async function bridgeFromStacks_toEVM( + ctx: SDKGlobalContext, info: Omit< BridgeFromStacksInput, "fromChain" | "toChain" | "fromToken" | "toToken" @@ -238,9 +242,10 @@ async function bridgeFromStacks_toEVM( ): Promise { const contractCallInfo = getStacksContractCallInfo( info.fromChain, - "cross-peg-out-endpoint-v2-01", + StacksContractName.EVMPegOutEndpoint, ) - const fromTokenContractInfo = getStacksTokenContractInfo( + const fromTokenContractInfo = await getStacksTokenContractInfo( + ctx, info.fromChain, info.fromToken, ) @@ -265,13 +270,14 @@ async function bridgeFromStacks_toEVM( "dest-chain-id": contractAssignedChainIdFromKnownChain(info.toChain), "settle-address": decodeHex(info.toAddress), }, - { deployerAddress: contractCallInfo.deployerAddress }, + contractCallInfo.executeOptions, ) return await info.sendTransaction(options) } async function bridgeFromStacks_toMeta( + ctx: SDKGlobalContext, info: Omit< BridgeFromStacksInput, "fromChain" | "toChain" | "fromToken" | "toToken" @@ -280,9 +286,10 @@ async function bridgeFromStacks_toMeta( ): Promise { const contractCallInfo = getStacksContractCallInfo( info.fromChain, - "meta-peg-out-endpoint-v2-04", + StacksContractName.MetaPegOutEndpoint, ) - const fromTokenContractInfo = getStacksTokenContractInfo( + const fromTokenContractInfo = await getStacksTokenContractInfo( + ctx, info.fromChain, info.fromToken, ) @@ -310,7 +317,7 @@ async function bridgeFromStacks_toMeta( "token-trait": `${fromTokenContractInfo.deployerAddress}.${fromTokenContractInfo.contractName}`, amount: numberToStacksContractNumber(info.amount), }, - { deployerAddress: contractCallInfo.deployerAddress }, + contractCallInfo.executeOptions, ) return await info.sendTransaction(options) diff --git a/src/xlinkSdkUtils/bridgeInfoFromBitcoin.ts b/src/xlinkSdkUtils/bridgeInfoFromBitcoin.ts index 3d15f4c..a07f061 100644 --- a/src/xlinkSdkUtils/bridgeInfoFromBitcoin.ts +++ b/src/xlinkSdkUtils/bridgeInfoFromBitcoin.ts @@ -1,17 +1,26 @@ import { getBtc2StacksFeeInfo } from "../bitcoinUtils/peggingHelpers" import { getStacks2EvmFeeInfo } from "../evmUtils/peggingHelpers" +import { getStacks2MetaFeeInfo } from "../metaUtils/peggingHelpers" import { BigNumber } from "../utils/BigNumber" +import { + getTransitStacksChainTransitStepInfos, + SwapRoute_WithExchangeRate_Public, +} from "../utils/SwapRouteHelpers" import { KnownRoute, + KnownRoute_FromBitcoin_ToBRC20, KnownRoute_FromBitcoin_ToEVM, + KnownRoute_FromBitcoin_ToRunes, KnownRoute_FromBitcoin_ToStacks, + KnownRoute_FromStacks_ToBRC20, + KnownRoute_FromStacks_ToRunes, } from "../utils/buildSupportedRoutes" import { UnsupportedBridgeRouteError } from "../utils/errors" import { assertExclude, checkNever } from "../utils/typeHelpers" import { PublicTransferProphetAggregated, transformToPublicTransferProphet, - transformToPublicTransferProphetAggregated, + transformToPublicTransferProphetAggregated2, } from "../utils/types/TransferProphet" import { KnownChainId, KnownTokenId } from "../utils/types/knownIds" import { supportedRoutes } from "./bridgeFromBitcoin" @@ -24,6 +33,7 @@ export interface BridgeInfoFromBitcoinInput { fromToken: TokenId toToken: TokenId amount: SDKNumber + swapRoute?: SwapRoute_WithExchangeRate_Public } export interface BridgeInfoFromBitcoinOutput @@ -54,7 +64,33 @@ export const bridgeInfoFromBitcoin = async ( KnownTokenId.isBitcoinToken(route.fromToken) && KnownTokenId.isEVMToken(route.toToken) ) { - return bridgeInfoFromBitcoin_toEVM({ + return bridgeInfoFromBitcoin_toEVM(ctx, { + ...info, + fromChain: route.fromChain, + toChain: route.toChain, + fromToken: route.fromToken, + toToken: route.toToken, + }) + } + } else if (KnownChainId.isBRC20Chain(route.toChain)) { + if ( + KnownTokenId.isBitcoinToken(route.fromToken) && + KnownTokenId.isBRC20Token(route.toToken) + ) { + return bridgeInfoFromBitcoin_toMeta(ctx, { + ...info, + fromChain: route.fromChain, + toChain: route.toChain, + fromToken: route.fromToken, + toToken: route.toToken, + }) + } + } else if (KnownChainId.isRunesChain(route.toChain)) { + if ( + KnownTokenId.isBitcoinToken(route.fromToken) && + KnownTokenId.isRunesToken(route.toToken) + ) { + return bridgeInfoFromBitcoin_toMeta(ctx, { ...info, fromChain: route.fromChain, toChain: route.toChain, @@ -62,13 +98,6 @@ export const bridgeInfoFromBitcoin = async ( toToken: route.toToken, }) } - } else if ( - KnownChainId.isBRC20Chain(route.toChain) || - KnownChainId.isRunesChain(route.toChain) - ) { - assertExclude(route.toChain, assertExclude.i()) - assertExclude(route.toChain, assertExclude.i()) - // TODO: bitcoin to brc20/runes is not supported yet } else { assertExclude(route.toChain, assertExclude.i()) checkNever(route) @@ -94,7 +123,9 @@ async function bridgeInfoFromBitcoin_toStacks( > & KnownRoute_FromBitcoin_ToStacks, ): Promise { - const step1 = await getBtc2StacksFeeInfo(info) + const step1 = await getBtc2StacksFeeInfo(info, { + swapRoute: info.swapRoute ?? null, + }) if (step1 == null) { throw new UnsupportedBridgeRouteError( info.fromChain, @@ -111,6 +142,7 @@ async function bridgeInfoFromBitcoin_toStacks( } async function bridgeInfoFromBitcoin_toEVM( + ctx: SDKGlobalContext, info: Omit< BridgeInfoFromBitcoinInput, "fromChain" | "toChain" | "fromToken" | "toToken" @@ -135,9 +167,21 @@ async function bridgeInfoFromBitcoin_toEVM( toToken: info.toToken, } + // TODO: add support for Bitcoin -> EVM with swap + if (info.swapRoute != null) { + throw new UnsupportedBridgeRouteError( + info.fromChain, + info.toChain, + info.fromToken, + info.toToken, + ) + } + const [step1, step2] = await Promise.all([ - getBtc2StacksFeeInfo(step1Route), - getStacks2EvmFeeInfo(step2Route), + getBtc2StacksFeeInfo(step1Route, { + swapRoute: info.swapRoute ?? null, + }), + getStacks2EvmFeeInfo(ctx, step2Route), ]) if (step1 == null || step2 == null) { throw new UnsupportedBridgeRouteError( @@ -148,22 +192,64 @@ async function bridgeInfoFromBitcoin_toEVM( ) } - const step1TransferProphet = transformToPublicTransferProphet( - step1Route, - info.amount, - step1, + return transformToPublicTransferProphetAggregated2( + [step1Route, step2Route], + [step1, step2], + BigNumber.from(info.amount), + BigNumber.ONE, + ) +} + +async function bridgeInfoFromBitcoin_toMeta( + ctx: SDKGlobalContext, + info: Omit< + BridgeInfoFromBitcoinInput, + "fromChain" | "toChain" | "fromToken" | "toToken" + > & + (KnownRoute_FromBitcoin_ToBRC20 | KnownRoute_FromBitcoin_ToRunes), +): Promise { + const transitStacksChain = + info.fromChain === KnownChainId.Bitcoin.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet + + const { step1ToStacksToken, step2FromStacksToken } = + await getTransitStacksChainTransitStepInfos(ctx, info) + + const step1Route: KnownRoute = { + fromChain: info.fromChain, + fromToken: info.fromToken, + toChain: transitStacksChain, + toToken: step1ToStacksToken, + } + const step2Route: + | KnownRoute_FromStacks_ToBRC20 + | KnownRoute_FromStacks_ToRunes = { + fromChain: transitStacksChain, + fromToken: step2FromStacksToken, + toChain: info.toChain as any, + toToken: info.toToken as any, + } + + const [step1, step2] = await Promise.all([ + getBtc2StacksFeeInfo(step1Route, { + swapRoute: info.swapRoute ?? null, + }), + getStacks2MetaFeeInfo(ctx, step2Route), + ]) + if (step1 == null || step2 == null) { + throw new UnsupportedBridgeRouteError( + info.fromChain, + info.toChain, + info.fromToken, + info.toToken, + ) + } + + return transformToPublicTransferProphetAggregated2( + [step1Route, step2Route], + [step1, step2], + BigNumber.from(info.amount), + BigNumber.from(info.swapRoute?.composedExchangeRate ?? BigNumber.ONE), ) - const step2TransferProphet = transformToPublicTransferProphet( - step2Route, - step1TransferProphet.toAmount, - step2, - ) - - return { - ...transformToPublicTransferProphetAggregated( - [step1TransferProphet, step2TransferProphet], - [BigNumber.ONE], - ), - transferProphets: [step1TransferProphet, step2TransferProphet], - } } diff --git a/src/xlinkSdkUtils/bridgeInfoFromEVM.ts b/src/xlinkSdkUtils/bridgeInfoFromEVM.ts index 12effe2..5b299cf 100644 --- a/src/xlinkSdkUtils/bridgeInfoFromEVM.ts +++ b/src/xlinkSdkUtils/bridgeInfoFromEVM.ts @@ -3,7 +3,7 @@ import { nativeCurrencyAddress } from "../evmUtils/addressHelpers" import { getEvm2StacksFeeInfo, getStacks2EvmFeeInfo, - toCorrespondingStacksToken, + evmTokenToCorrespondingStacksToken, } from "../evmUtils/peggingHelpers" import { getEVMTokenContractInfo } from "../evmUtils/xlinkContractHelpers" import { getStacks2MetaFeeInfo } from "../metaUtils/peggingHelpers" @@ -23,7 +23,7 @@ import { assertExclude, checkNever } from "../utils/typeHelpers" import { PublicTransferProphetAggregated, transformToPublicTransferProphet, - transformToPublicTransferProphetAggregated, + transformToPublicTransferProphetAggregated2, } from "../utils/types/TransferProphet" import { KnownChainId, KnownTokenId } from "../utils/types/knownIds" import { supportedRoutes } from "./bridgeFromEVM" @@ -173,7 +173,9 @@ async function bridgeInfoFromEVM_toBitcoin( const transitStacksChain = KnownChainId.isEVMMainnetChain(info.fromChain) ? KnownChainId.Stacks.Mainnet : KnownChainId.Stacks.Testnet - const transitStacksToken = await toCorrespondingStacksToken(info.fromToken) + const transitStacksToken = await evmTokenToCorrespondingStacksToken( + info.fromToken, + ) if (transitStacksToken == null) { throw new UnsupportedBridgeRouteError( info.fromChain, @@ -198,7 +200,9 @@ async function bridgeInfoFromEVM_toBitcoin( const [step1, step2] = await Promise.all([ getEvm2StacksFeeInfo(ctx, step1Route), - getStacks2BtcFeeInfo(step2Route), + getStacks2BtcFeeInfo(step2Route, { + swappedFromRoute: step1Route, + }), ]) if (step1 == null || step2 == null) { throw new UnsupportedBridgeRouteError( @@ -209,24 +213,12 @@ async function bridgeInfoFromEVM_toBitcoin( ) } - const step1TransferProphet = transformToPublicTransferProphet( - step1Route, - info.amount, - step1, + return transformToPublicTransferProphetAggregated2( + [step1Route, step2Route], + [step1, step2], + BigNumber.from(info.amount), + BigNumber.ONE, ) - const step2TransferProphet = transformToPublicTransferProphet( - step2Route, - step1TransferProphet.toAmount, - step2, - ) - - return { - ...transformToPublicTransferProphetAggregated( - [step1TransferProphet, step2TransferProphet], - [BigNumber.ONE], - ), - transferProphets: [step1TransferProphet, step2TransferProphet], - } } async function bridgeInfoFromEVM_toEVM( @@ -246,7 +238,9 @@ async function bridgeInfoFromEVM_toEVM( const transitStacksChain = KnownChainId.isEVMMainnetChain(info.fromChain) ? KnownChainId.Stacks.Mainnet : KnownChainId.Stacks.Testnet - const transitStacksToken = await toCorrespondingStacksToken(info.fromToken) + const transitStacksToken = await evmTokenToCorrespondingStacksToken( + info.fromToken, + ) if ( transitStacksToken == null || evmTokenContractCallInfo?.tokenContractAddress === nativeCurrencyAddress @@ -274,7 +268,7 @@ async function bridgeInfoFromEVM_toEVM( const [step1, step2] = await Promise.all([ getEvm2StacksFeeInfo(ctx, step1Route), - getStacks2EvmFeeInfo(step2Route), + getStacks2EvmFeeInfo(ctx, step2Route), ]) if (step1 == null || step2 == null) { throw new UnsupportedBridgeRouteError( @@ -285,24 +279,12 @@ async function bridgeInfoFromEVM_toEVM( ) } - const step1TransferProphet = transformToPublicTransferProphet( - step1Route, - info.amount, - step1, + return transformToPublicTransferProphetAggregated2( + [step1Route, step2Route], + [step1, step2], + BigNumber.from(info.amount), + BigNumber.ONE, ) - const step2TransferProphet = transformToPublicTransferProphet( - step2Route, - step1TransferProphet.toAmount, - step2, - ) - - return { - ...transformToPublicTransferProphetAggregated( - [step1TransferProphet, step2TransferProphet], - [BigNumber.ONE], - ), - transferProphets: [step1TransferProphet, step2TransferProphet], - } } async function bridgeInfoFromEVM_toMeta( @@ -322,7 +304,9 @@ async function bridgeInfoFromEVM_toMeta( const transitStacksChain = KnownChainId.isEVMMainnetChain(info.fromChain) ? KnownChainId.Stacks.Mainnet : KnownChainId.Stacks.Testnet - const transitStacksToken = await toCorrespondingStacksToken(info.fromToken) + const transitStacksToken = await evmTokenToCorrespondingStacksToken( + info.fromToken, + ) if ( transitStacksToken == null || evmTokenContractCallInfo?.tokenContractAddress === nativeCurrencyAddress @@ -363,22 +347,10 @@ async function bridgeInfoFromEVM_toMeta( ) } - const step1TransferProphet = transformToPublicTransferProphet( - step1Route, - info.amount, - step1, + return transformToPublicTransferProphetAggregated2( + [step1Route, step2Route], + [step1, step2], + BigNumber.from(info.amount), + BigNumber.ONE, ) - const step2TransferProphet = transformToPublicTransferProphet( - step2Route, - step1TransferProphet.toAmount, - step2, - ) - - return { - ...transformToPublicTransferProphetAggregated( - [step1TransferProphet, step2TransferProphet], - [BigNumber.ONE], - ), - transferProphets: [step1TransferProphet, step2TransferProphet], - } } diff --git a/src/xlinkSdkUtils/bridgeInfoFromMeta.ts b/src/xlinkSdkUtils/bridgeInfoFromMeta.ts new file mode 100644 index 0000000..9e3f530 --- /dev/null +++ b/src/xlinkSdkUtils/bridgeInfoFromMeta.ts @@ -0,0 +1,354 @@ +import { getStacks2BtcFeeInfo } from "../bitcoinUtils/peggingHelpers" +import { getStacks2EvmFeeInfo } from "../evmUtils/peggingHelpers" +import { + getMeta2StacksFeeInfo, + getStacks2MetaFeeInfo, +} from "../metaUtils/peggingHelpers" +import { BigNumber } from "../utils/BigNumber" +import { + getTransitStacksChainTransitStepInfos, + SwapRoute_WithExchangeRate_Public, +} from "../utils/SwapRouteHelpers" +import { + KnownRoute, + KnownRoute_FromBRC20_ToBitcoin, + KnownRoute_FromBRC20_ToBRC20, + KnownRoute_FromBRC20_ToEVM, + KnownRoute_FromBRC20_ToRunes, + KnownRoute_FromBRC20_ToStacks, + KnownRoute_FromRunes_ToBitcoin, + KnownRoute_FromRunes_ToBRC20, + KnownRoute_FromRunes_ToEVM, + KnownRoute_FromRunes_ToRunes, + KnownRoute_FromRunes_ToStacks, + KnownRoute_FromStacks_ToBRC20, + KnownRoute_FromStacks_ToRunes, + KnownRoute_WithMetaProtocol, +} from "../utils/buildSupportedRoutes" +import { UnsupportedBridgeRouteError } from "../utils/errors" +import { assertExclude, checkNever } from "../utils/typeHelpers" +import { + PublicTransferProphetAggregated, + transformToPublicTransferProphet, + transformToPublicTransferProphetAggregated2, +} from "../utils/types/TransferProphet" +import { KnownChainId, KnownTokenId } from "../utils/types/knownIds" +import { ChainId, SDKNumber, TokenId } from "./types" +import { SDKGlobalContext } from "./types.internal" + +export interface BridgeInfoFromMetaInput { + fromChain: ChainId + toChain: ChainId + fromToken: TokenId + toToken: TokenId + amount: SDKNumber + swapRoute?: SwapRoute_WithExchangeRate_Public +} + +export interface BridgeInfoFromMetaOutput + extends PublicTransferProphetAggregated {} + +export const bridgeInfoFromMeta = async ( + ctx: SDKGlobalContext, + info: BridgeInfoFromMetaInput, +): Promise => { + const isMetaProtocolChain = ( + chain: ChainId, + ): chain is KnownChainId.BRC20Chain | KnownChainId.RunesChain => + KnownChainId.isBRC20Chain(chain) || KnownChainId.isRunesChain(chain) + const isMetaProtocolToken = ( + token: TokenId, + ): token is KnownTokenId.BRC20Token | KnownTokenId.RunesToken => + KnownTokenId.isBRC20Token(token) || KnownTokenId.isRunesToken(token) + + const route = info as any as KnownRoute_WithMetaProtocol + + if (isMetaProtocolChain(route.fromChain)) { + if (KnownChainId.isStacksChain(route.toChain)) { + if ( + isMetaProtocolToken(route.fromToken) && + KnownTokenId.isStacksToken(route.toToken) + ) { + return bridgeInfoFromMeta_toStacks(ctx, { + ...info, + fromChain: route.fromChain as KnownChainId.BRC20Chain, + fromToken: route.fromToken as KnownTokenId.BRC20Token, + toChain: route.toChain, + toToken: route.toToken, + }) + } + } else if (KnownChainId.isEVMChain(route.toChain)) { + if ( + isMetaProtocolToken(route.fromToken) && + KnownTokenId.isEVMToken(route.toToken) + ) { + return bridgeInfoFromMeta_toEVM(ctx, { + ...info, + fromChain: route.fromChain as KnownChainId.BRC20Chain, + fromToken: route.fromToken as KnownTokenId.BRC20Token, + toChain: route.toChain, + toToken: route.toToken, + }) + } + } else if (KnownChainId.isBitcoinChain(route.toChain)) { + if ( + isMetaProtocolToken(route.fromToken) && + KnownTokenId.isBitcoinToken(route.toToken) + ) { + return bridgeInfoFromMeta_toBitcoin(ctx, { + ...info, + fromChain: route.fromChain as KnownChainId.BRC20Chain, + fromToken: route.fromToken as KnownTokenId.BRC20Token, + toChain: route.toChain, + toToken: route.toToken, + }) + } + } else if (KnownChainId.isBRC20Chain(route.toChain)) { + if ( + isMetaProtocolToken(route.fromToken) && + KnownTokenId.isBRC20Token(route.toToken) + ) { + return bridgeInfoFromMeta_toMeta(ctx, { + ...info, + fromChain: route.fromChain as KnownChainId.BRC20Chain, + fromToken: route.fromToken as KnownTokenId.BRC20Token, + toChain: route.toChain, + toToken: route.toToken, + }) + } + } else if (KnownChainId.isRunesChain(route.toChain)) { + if ( + isMetaProtocolToken(route.fromToken) && + KnownTokenId.isRunesToken(route.toToken) + ) { + return bridgeInfoFromMeta_toMeta(ctx, { + ...info, + fromChain: route.fromChain as KnownChainId.BRC20Chain, + fromToken: route.fromToken as KnownTokenId.BRC20Token, + toChain: route.toChain, + toToken: route.toToken, + }) + } + } else { + checkNever(route.toChain) + } + } else { + assertExclude(route.fromChain, assertExclude.i()) + assertExclude(route.fromChain, assertExclude.i()) + assertExclude(route.fromChain, assertExclude.i()) + checkNever(route) + } + + throw new UnsupportedBridgeRouteError( + info.fromChain, + info.toChain, + info.fromToken, + info.toToken, + ) +} + +async function bridgeInfoFromMeta_toStacks( + ctx: SDKGlobalContext, + info: Omit< + BridgeInfoFromMetaInput, + "fromChain" | "toChain" | "fromToken" | "toToken" + > & + (KnownRoute_FromBRC20_ToStacks | KnownRoute_FromRunes_ToStacks), +): Promise { + const step1 = await getMeta2StacksFeeInfo(ctx, info, { + swapRoute: info.swapRoute ?? null, + }) + if (step1 == null) { + throw new UnsupportedBridgeRouteError( + info.fromChain, + info.toChain, + info.fromToken, + info.toToken, + ) + } + + return { + ...transformToPublicTransferProphet(info, info.amount, step1), + transferProphets: [], + } +} + +async function bridgeInfoFromMeta_toEVM( + ctx: SDKGlobalContext, + info: Omit< + BridgeInfoFromMetaInput, + "fromChain" | "toChain" | "fromToken" | "toToken" + > & + (KnownRoute_FromBRC20_ToEVM | KnownRoute_FromRunes_ToEVM), +): Promise { + const transitStacksChainId = + info.fromChain === KnownChainId.BRC20.Mainnet || + info.fromChain === KnownChainId.Runes.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet + + const { step1ToStacksToken, step2FromStacksToken } = + await getTransitStacksChainTransitStepInfos(ctx, info) + + const step1Route = { + fromChain: info.fromChain as KnownChainId.BRC20Chain, + fromToken: info.fromToken as KnownTokenId.BRC20Token, + toChain: transitStacksChainId, + toToken: step1ToStacksToken, + } satisfies KnownRoute_WithMetaProtocol + const step2Route: KnownRoute = { + fromChain: transitStacksChainId, + fromToken: step2FromStacksToken, + toChain: info.toChain, + toToken: info.toToken, + } satisfies KnownRoute_WithMetaProtocol + + // TODO: add support for Meta -> EVM with swap + if (info.swapRoute != null) { + throw new UnsupportedBridgeRouteError( + info.fromChain, + info.toChain, + info.fromToken, + info.toToken, + ) + } + + const [step1, step2] = await Promise.all([ + getMeta2StacksFeeInfo(ctx, step1Route, { + swapRoute: info.swapRoute ?? null, + }), + getStacks2EvmFeeInfo(ctx, step2Route), + ]) + if (step1 == null || step2 == null) { + throw new UnsupportedBridgeRouteError( + info.fromChain, + info.toChain, + info.fromToken, + info.toToken, + ) + } + + return transformToPublicTransferProphetAggregated2( + [step1Route, step2Route], + [step1, step2], + BigNumber.from(info.amount), + BigNumber.ONE, + ) +} + +async function bridgeInfoFromMeta_toBitcoin( + ctx: SDKGlobalContext, + info: Omit< + BridgeInfoFromMetaInput, + "fromChain" | "toChain" | "fromToken" | "toToken" + > & + (KnownRoute_FromBRC20_ToBitcoin | KnownRoute_FromRunes_ToBitcoin), +): Promise { + const transitStacksChainId = + info.fromChain === KnownChainId.BRC20.Mainnet || + info.fromChain === KnownChainId.Runes.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet + + const { step1ToStacksToken, step2FromStacksToken } = + await getTransitStacksChainTransitStepInfos(ctx, info) + + const step1Route = { + fromChain: info.fromChain as KnownChainId.BRC20Chain, + fromToken: info.fromToken as KnownTokenId.BRC20Token, + toChain: transitStacksChainId, + toToken: step1ToStacksToken, + } satisfies KnownRoute_WithMetaProtocol + const step2Route: KnownRoute = { + fromChain: transitStacksChainId, + fromToken: step2FromStacksToken, + toChain: info.toChain, + toToken: info.toToken, + } satisfies KnownRoute_WithMetaProtocol + + const [step1, step2] = await Promise.all([ + getMeta2StacksFeeInfo(ctx, step1Route, { + swapRoute: info.swapRoute ?? null, + }), + getStacks2BtcFeeInfo(step2Route, { + swappedFromRoute: step1Route, + }), + ]) + if (step1 == null || step2 == null) { + throw new UnsupportedBridgeRouteError( + info.fromChain, + info.toChain, + info.fromToken, + info.toToken, + ) + } + + return transformToPublicTransferProphetAggregated2( + [step1Route, step2Route], + [step1, step2], + BigNumber.from(info.amount), + BigNumber.from(info.swapRoute?.composedExchangeRate ?? BigNumber.ONE), + ) +} + +async function bridgeInfoFromMeta_toMeta( + ctx: SDKGlobalContext, + info: Omit< + BridgeInfoFromMetaInput, + "fromChain" | "toChain" | "fromToken" | "toToken" + > & + ( + | KnownRoute_FromBRC20_ToBRC20 + | KnownRoute_FromBRC20_ToRunes + | KnownRoute_FromRunes_ToBRC20 + | KnownRoute_FromRunes_ToRunes + ), +): Promise { + const transitStacksChain = + info.fromChain === KnownChainId.BRC20.Mainnet || + info.fromChain === KnownChainId.Runes.Mainnet + ? KnownChainId.Stacks.Mainnet + : KnownChainId.Stacks.Testnet + + const { step1ToStacksToken, step2FromStacksToken } = + await getTransitStacksChainTransitStepInfos(ctx, info) + + const step1Route: + | KnownRoute_FromBRC20_ToStacks + | KnownRoute_FromRunes_ToStacks = { + fromChain: info.fromChain as any, + fromToken: info.fromToken as any, + toChain: transitStacksChain, + toToken: step1ToStacksToken, + } satisfies KnownRoute_WithMetaProtocol + const step2Route: + | KnownRoute_FromStacks_ToBRC20 + | KnownRoute_FromStacks_ToRunes = { + fromChain: transitStacksChain, + fromToken: step2FromStacksToken, + toChain: info.toChain as any, + toToken: info.toToken as any, + } + + const [step1, step2] = await Promise.all([ + getMeta2StacksFeeInfo(ctx, step1Route, { + swapRoute: info.swapRoute ?? null, + }), + getStacks2MetaFeeInfo(ctx, step2Route), + ]) + if (step1 == null || step2 == null) { + throw new UnsupportedBridgeRouteError( + info.fromChain, + info.toChain, + info.fromToken, + info.toToken, + ) + } + + return transformToPublicTransferProphetAggregated2( + [step1Route, step2Route], + [step1, step2], + BigNumber.from(info.amount), + BigNumber.from(info.swapRoute?.composedExchangeRate ?? BigNumber.ONE), + ) +} diff --git a/src/xlinkSdkUtils/bridgeInfoFromStacks.ts b/src/xlinkSdkUtils/bridgeInfoFromStacks.ts index 0be878b..1e6d39c 100644 --- a/src/xlinkSdkUtils/bridgeInfoFromStacks.ts +++ b/src/xlinkSdkUtils/bridgeInfoFromStacks.ts @@ -54,7 +54,7 @@ export async function bridgeInfoFromStacks( KnownTokenId.isStacksToken(route.fromToken) && KnownTokenId.isEVMToken(route.toToken) ) { - return bridgeInfoFromStacks_toEVM({ + return bridgeInfoFromStacks_toEVM(ctx, { ...info, fromChain: route.fromChain, toChain: route.toChain, @@ -113,7 +113,9 @@ async function bridgeInfoFromStacks_toBitcoin( > & KnownRoute_FromStacks_ToBitcoin, ): Promise { - const step1 = await getStacks2BtcFeeInfo(info) + const step1 = await getStacks2BtcFeeInfo(info, { + swappedFromRoute: null, + }) if (step1 == null) { throw new UnsupportedBridgeRouteError( info.fromChain, @@ -130,13 +132,14 @@ async function bridgeInfoFromStacks_toBitcoin( } async function bridgeInfoFromStacks_toEVM( + ctx: SDKGlobalContext, info: Omit< BridgeInfoFromStacksInput, "fromChain" | "toChain" | "fromToken" | "toToken" > & KnownRoute_FromStacks_ToEVM, ): Promise { - const step1 = await getStacks2EvmFeeInfo(info) + const step1 = await getStacks2EvmFeeInfo(ctx, info) if (step1 == null) { throw new UnsupportedBridgeRouteError( info.fromChain, diff --git a/src/xlinkSdkUtils/estimateBridgeTransactionFromBitcoin.ts b/src/xlinkSdkUtils/estimateBridgeTransactionFromBitcoin.ts index 097ab60..a0101bb 100644 --- a/src/xlinkSdkUtils/estimateBridgeTransactionFromBitcoin.ts +++ b/src/xlinkSdkUtils/estimateBridgeTransactionFromBitcoin.ts @@ -2,14 +2,29 @@ import { getBTCPegInAddress } from "../bitcoinUtils/btcAddresses" import { ReselectSpendableUTXOsFn } from "../bitcoinUtils/prepareTransaction" import { createBridgeOrder_BitcoinToEVM, + createBridgeOrder_BitcoinToMeta, createBridgeOrder_BitcoinToStacks, -} from "../stacksUtils/createBridgeOrder" -import { numberToStacksContractNumber } from "../stacksUtils/xlinkContractHelpers" -import { UnsupportedBridgeRouteError } from "../utils/errors" +} from "../stacksUtils/createBridgeOrderFromBitcoin" +import { BigNumber } from "../utils/BigNumber" +import { + KnownRoute_FromBitcoin_ToBRC20, + KnownRoute_FromBitcoin_ToEVM, + KnownRoute_FromBitcoin_ToRunes, + KnownRoute_FromBitcoin_ToStacks, +} from "../utils/buildSupportedRoutes" +import { + InvalidMethodParametersError, + UnsupportedBridgeRouteError, +} from "../utils/errors" import { assertExclude, checkNever } from "../utils/typeHelpers" -import { KnownChainId, KnownTokenId } from "../utils/types/knownIds" +import { + _knownChainIdToErrorMessagePart, + KnownChainId, + KnownTokenId, +} from "../utils/types/knownIds" import { prepareBitcoinTransaction, supportedRoutes } from "./bridgeFromBitcoin" import { ChainId, SDKNumber, TokenId, toSDKNumberOrUndefined } from "./types" +import { SwapRoute_WithMinimumAmountsToReceive_Public } from "../utils/SwapRouteHelpers" import { SDKGlobalContext } from "./types.internal" export interface EstimateBridgeTransactionFromBitcoinInput { @@ -20,7 +35,12 @@ export interface EstimateBridgeTransactionFromBitcoinInput { fromAddress: string fromAddressScriptPubKey: Uint8Array toAddress: string + /** + * **Required** when `toChain` is one of bitcoin chains + */ + toAddressScriptPubKey?: Uint8Array amount: SDKNumber + swapRoute?: SwapRoute_WithMinimumAmountsToReceive_Public networkFeeRate: bigint reselectSpendableUTXOs: ReselectSpendableUTXOsFn } @@ -63,13 +83,32 @@ export async function estimateBridgeTransactionFromBitcoin( toToken: route.toToken, }) } - } else if ( - KnownChainId.isBRC20Chain(route.toChain) || - KnownChainId.isRunesChain(route.toChain) - ) { - assertExclude(route.toChain, assertExclude.i()) - assertExclude(route.toChain, assertExclude.i()) - // TODO: bitcoin to brc20/runes is not supported yet + } else if (KnownChainId.isBRC20Chain(route.toChain)) { + if ( + KnownTokenId.isBitcoinToken(route.fromToken) && + KnownTokenId.isBRC20Token(route.toToken) + ) { + return estimateFromBitcoin_toMeta(ctx, { + ...info, + fromChain: route.fromChain, + toChain: route.toChain, + fromToken: route.fromToken, + toToken: route.toToken, + }) + } + } else if (KnownChainId.isRunesChain(route.toChain)) { + if ( + KnownTokenId.isBitcoinToken(route.fromToken) && + KnownTokenId.isRunesToken(route.toToken) + ) { + return estimateFromBitcoin_toMeta(ctx, { + ...info, + fromChain: route.fromChain, + toChain: route.toChain, + fromToken: route.fromToken, + toToken: route.toToken, + }) + } } else { assertExclude(route.toChain, assertExclude.i()) checkNever(route) @@ -89,16 +128,12 @@ export async function estimateBridgeTransactionFromBitcoin( } async function estimateFromBitcoin_toStacks( - sdkContext: Pick, + sdkContext: SDKGlobalContext, info: Omit< EstimateBridgeTransactionFromBitcoinInput, "fromChain" | "toChain" | "fromToken" | "toToken" - > & { - fromChain: KnownChainId.BitcoinChain - toChain: KnownChainId.StacksChain - fromToken: KnownTokenId.BitcoinToken - toToken: KnownTokenId.StacksToken - }, + > & + KnownRoute_FromBitcoin_ToStacks, ): Promise { const pegInAddress = getBTCPegInAddress(info.fromChain, info.toChain) if (pegInAddress == null) { @@ -109,14 +144,21 @@ async function estimateFromBitcoin_toStacks( ) } - const createdOrder = await createBridgeOrder_BitcoinToStacks({ + const createdOrder = await createBridgeOrder_BitcoinToStacks(sdkContext, { fromChain: info.fromChain, fromBitcoinScriptPubKey: info.fromAddressScriptPubKey, toChain: info.toChain, toToken: info.toToken, toStacksAddress: info.toAddress, - swapSlippedAmount: numberToStacksContractNumber(info.amount), - swapRoute: [], + swap: + info.swapRoute == null + ? undefined + : { + ...info.swapRoute, + minimumAmountsToReceive: BigNumber.from( + info.swapRoute.minimumAmountsToReceive, + ), + }, }) if (createdOrder == null) { throw new UnsupportedBridgeRouteError( @@ -139,16 +181,12 @@ async function estimateFromBitcoin_toStacks( } async function estimateFromBitcoin_toEVM( - sdkContext: Pick, + sdkContext: SDKGlobalContext, info: Omit< EstimateBridgeTransactionFromBitcoinInput, "fromChain" | "toChain" | "fromToken" | "toToken" - > & { - fromChain: KnownChainId.BitcoinChain - toChain: KnownChainId.EVMChain - fromToken: KnownTokenId.BitcoinToken - toToken: KnownTokenId.EVMToken - }, + > & + KnownRoute_FromBitcoin_ToEVM, ): Promise { const pegInAddress = getBTCPegInAddress(info.fromChain, info.toChain) if (pegInAddress == null) { @@ -159,14 +197,88 @@ async function estimateFromBitcoin_toEVM( ) } - const createdOrder = await createBridgeOrder_BitcoinToEVM({ + const createdOrder = await createBridgeOrder_BitcoinToEVM(sdkContext, { fromChain: info.fromChain, toChain: info.toChain, toToken: info.toToken, fromBitcoinScriptPubKey: info.fromAddressScriptPubKey, toEVMAddress: info.toAddress, - swapSlippedAmount: numberToStacksContractNumber(info.amount), - swapRoute: [], + swap: + info.swapRoute == null + ? undefined + : { + ...info.swapRoute, + minimumAmountsToReceive: BigNumber.from( + info.swapRoute.minimumAmountsToReceive, + ), + }, + }) + if (createdOrder == null) { + throw new UnsupportedBridgeRouteError( + info.fromChain, + info.toChain, + KnownTokenId.Bitcoin.BTC, + ) + } + + const resp = await prepareBitcoinTransaction(sdkContext, { + ...info, + orderData: createdOrder.data, + pegInAddress, + }) + + return { + fee: toSDKNumberOrUndefined(resp.fee), + estimatedVSize: toSDKNumberOrUndefined(resp.estimatedVSize), + } +} + +async function estimateFromBitcoin_toMeta( + sdkContext: SDKGlobalContext, + info: Omit< + EstimateBridgeTransactionFromBitcoinInput, + "fromChain" | "toChain" | "fromToken" | "toToken" + > & + (KnownRoute_FromBitcoin_ToBRC20 | KnownRoute_FromBitcoin_ToRunes), +): Promise { + if (info.toAddressScriptPubKey == null) { + throw new InvalidMethodParametersError( + [ + "XLinkSDK", + `estimateBridgeTransactionFromBitcoin (to ${_knownChainIdToErrorMessagePart(info.toChain)})`, + ], + [ + { + name: "toAddressScriptPubKey", + expected: "Uint8Array", + received: "undefined", + }, + ], + ) + } + + const pegInAddress = getBTCPegInAddress(info.fromChain, info.toChain) + if (pegInAddress == null) { + throw new UnsupportedBridgeRouteError( + info.fromChain, + info.toChain, + KnownTokenId.Bitcoin.BTC, + ) + } + + const createdOrder = await createBridgeOrder_BitcoinToMeta(sdkContext, { + ...info, + fromBitcoinScriptPubKey: info.fromAddressScriptPubKey, + toBitcoinScriptPubKey: info.toAddressScriptPubKey, + swap: + info.swapRoute == null + ? undefined + : { + ...info.swapRoute, + minimumAmountsToReceive: BigNumber.from( + info.swapRoute.minimumAmountsToReceive, + ), + }, }) if (createdOrder == null) { throw new UnsupportedBridgeRouteError( diff --git a/src/xlinkSdkUtils/types.internal.ts b/src/xlinkSdkUtils/types.internal.ts index 3c7cf89..c83f820 100644 --- a/src/xlinkSdkUtils/types.internal.ts +++ b/src/xlinkSdkUtils/types.internal.ts @@ -1,10 +1,12 @@ import { Client } from "viem" import { EVMOnChainAddresses } from "../evmUtils/evmContractAddresses" -import { KnownChainId } from "../utils/types/knownIds" import type { BRC20SupportedRoute, RunesSupportedRoute, } from "../metaUtils/xlinkContractHelpers" +import { StacksTokenInfo } from "../stacksUtils/xlinkContractHelpers" +import { KnownChainId } from "../utils/types/knownIds" +import { EVMAddress } from "./types" export interface SDKGlobalContextCache { get: (key: K) => T | null @@ -16,22 +18,33 @@ export interface SDKGlobalContext { backendAPI: { runtimeEnv: "prod" | "dev" } + stacks: { + tokensCache?: SDKGlobalContextCache< + KnownChainId.StacksChain, + Promise + > + } + btc: { + ignoreValidateResult?: boolean + } brc20: { routesConfigCache?: SDKGlobalContextCache< - string, + KnownChainId.BRC20Chain, Promise > + ignoreValidateResult?: boolean } runes: { routesConfigCache?: SDKGlobalContextCache< - string, + KnownChainId.RunesChain, Promise > + ignoreValidateResult?: boolean } evm: { enableMulticall?: boolean onChainConfigCache?: SDKGlobalContextCache< - string, + `${KnownChainId.EVMChain}:${EVMAddress}`, Promise > viemClients: Partial> diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..fe6c727 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsup" + +export default defineConfig({ + outDir: "lib", + format: ["cjs", "esm"], + treeshake: true, + sourcemap: true, + dts: true, + env: { + ENV_NAME: process.env.ENV_NAME ?? "prod", + }, +})