feat: implement cross chain swap

This commit is contained in:
c4605
2024-11-14 16:35:28 +01:00
parent 0f6d129bd1
commit f2d89a6eb3
45 changed files with 3921 additions and 1039 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -1,17 +1,21 @@
import { defineContract } from "../smartContractHelpers/codegenImport"; import { defineContract } from "../smartContractHelpers/codegenImport";
import { btcPegInEndpointV205 } from "./contract_xlink_btc-peg-in-endpoint-v2-05" 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 { btcPegOutEndpointV201 } from "./contract_xlink_btc-peg-out-endpoint-v2-01"
import { crossPegInEndpointV204 } from "./contract_xlink_cross-peg-in-endpoint-v2-04" import { crossPegInEndpointV204 } from "./contract_xlink_cross-peg-in-endpoint-v2-04"
import { crossPegOutEndpointV201 } from "./contract_xlink_cross-peg-out-endpoint-v2-01" import { crossPegOutEndpointV201 } from "./contract_xlink_cross-peg-out-endpoint-v2-01"
import { metaPegInEndpointV204 } from "./contract_xlink_meta-peg-in-endpoint-v2-04" 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" import { metaPegOutEndpointV204 } from "./contract_xlink_meta-peg-out-endpoint-v2-04"
export const xlinkContracts = defineContract({ export const xlinkContracts = defineContract({
...btcPegInEndpointV205, ...btcPegInEndpointV205,
...btcPegInEndpointV207Swap,
...btcPegOutEndpointV201, ...btcPegOutEndpointV201,
...crossPegInEndpointV204, ...crossPegInEndpointV204,
...crossPegOutEndpointV201, ...crossPegOutEndpointV201,
...metaPegInEndpointV204, ...metaPegInEndpointV204,
...metaPegInEndpointV206Swap,
...metaPegOutEndpointV204 ...metaPegOutEndpointV204
}); });

View File

@@ -41,9 +41,9 @@
"gen": "pnpm run gen:stacksContract", "gen": "pnpm run gen:stacksContract",
"docs": "typedoc", "docs": "typedoc",
"docs:watch": "typedoc --watch", "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", "prepare": "pnpm run build",
"test": "vitest --exclude lib" "test": "vitest --exclude lib --run"
}, },
"dependencies": { "dependencies": {
"@c4/btc-utils": "^0.3.1", "@c4/btc-utils": "^0.3.1",

View File

@@ -1,32 +1,52 @@
import { generateContracts } from "clarity-codegen/lib/generate" import { generateContracts } from "clarity-codegen/lib/generate"
import * as path from "node:path" import * as path from "node:path"
import { STACKS_MAINNET } from "../src/config"
import { import {
stxContractDeployers, contractNameOverrides,
envName,
STACKS_MAINNET,
STACKS_TESTNET,
} from "../src/config"
import {
StacksContractName,
stxContractAddresses,
xlinkContractsMultisigMainnet, xlinkContractsMultisigMainnet,
xlinkContractsMultisigTestnet,
} from "../src/stacksUtils/stxContractAddresses" } from "../src/stacksUtils/stxContractAddresses"
import { KnownChainId } from "../src/utils/types/knownIds" import { KnownChainId } from "../src/utils/types/knownIds"
;(async function main(): Promise<void> { ;(async function main(): Promise<void> {
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( await generateContracts(
process.env.STACKS_CORE_API_URL ?? STACKS_MAINNET.coreApiUrl, process.env.STACKS_CORE_API_URL ?? fallbackStacksNetwork.coreApiUrl,
contractName => { contractName => {
return ( return (
stxContractDeployers[ stxContractAddresses[contractName as StacksContractName]?.[
contractName as keyof typeof stxContractDeployers stacksChainId
]?.[KnownChainId.Stacks.Mainnet]?.deployerAddress ?? ]?.deployerAddress ?? fallbackDeployerAddress
xlinkContractsMultisigMainnet
) )
}, },
[ [
"btc-peg-in-endpoint-v2-05", "btc-peg-in-endpoint-v2-05",
"btc-peg-in-endpoint-v2-07-swap",
"btc-peg-out-endpoint-v2-01", "btc-peg-out-endpoint-v2-01",
"cross-peg-in-endpoint-v2-04", "cross-peg-in-endpoint-v2-04",
"cross-peg-out-endpoint-v2-01", "cross-peg-out-endpoint-v2-01",
"meta-peg-in-endpoint-v2-04", "meta-peg-in-endpoint-v2-04",
"meta-peg-in-endpoint-v2-06-swap",
"meta-peg-out-endpoint-v2-04", "meta-peg-out-endpoint-v2-04",
], ],
path.resolve(__dirname, "../generated/smartContract/"), path.resolve(__dirname, "../generated/smartContract/"),
"xlink", "xlink",
"../smartContractHelpers/codegenImport", "../smartContractHelpers/codegenImport",
contractNameOverrides,
) )
})().catch(console.error) })().catch(console.error)

View File

@@ -14,6 +14,8 @@ async function print(matchers: { chain: string[] }): Promise<void> {
backendAPI: { backendAPI: {
runtimeEnv: "prod", runtimeEnv: "prod",
}, },
stacks: {},
btc: {},
brc20: {}, brc20: {},
runes: {}, runes: {},
evm: { evm: {

View File

@@ -1,7 +1,11 @@
import { Client } from "viem" import { Client } from "viem"
import { getBTCPegInAddress } from "./bitcoinUtils/btcAddresses" import { getBTCPegInAddress } from "./bitcoinUtils/btcAddresses"
import { nativeCurrencyAddress } from "./evmUtils/addressHelpers" import { nativeCurrencyAddress } from "./evmUtils/addressHelpers"
import { defaultEvmClients } from "./evmUtils/evmClients" import {
defaultEvmClients,
evmChainIdFromKnownChainId,
evmChainIdToKnownChainId,
} from "./evmUtils/evmClients"
import { import {
getEVMContractCallInfo, getEVMContractCallInfo,
getEVMToken, getEVMToken,
@@ -124,6 +128,15 @@ export interface XLinkSDKOptions {
backendAPI?: { backendAPI?: {
runtimeEnv?: "prod" | "dev" runtimeEnv?: "prod" | "dev"
} }
btc?: {
ignoreValidateResult?: boolean
}
brc20?: {
ignoreValidateResult?: boolean
}
runes?: {
ignoreValidateResult?: boolean
}
} }
evm?: { evm?: {
/** /**
@@ -160,11 +173,22 @@ export class XLinkSDK {
...options.__experimental?.backendAPI, ...options.__experimental?.backendAPI,
runtimeEnv: options.__experimental?.backendAPI?.runtimeEnv ?? "prod", runtimeEnv: options.__experimental?.backendAPI?.runtimeEnv ?? "prod",
}, },
stacks: {
tokensCache: new Map(),
},
btc: {
ignoreValidateResult:
options.__experimental?.btc?.ignoreValidateResult ?? false,
},
brc20: { brc20: {
routesConfigCache: new Map(), routesConfigCache: new Map(),
ignoreValidateResult:
options.__experimental?.brc20?.ignoreValidateResult ?? false,
}, },
runes: { runes: {
routesConfigCache: new Map(), routesConfigCache: new Map(),
ignoreValidateResult:
options.__experimental?.runes?.ignoreValidateResult ?? false,
}, },
evm: { evm: {
onChainConfigCache: cacheEVMOnChainConfig ? new Map() : undefined, onChainConfigCache: cacheEVMOnChainConfig ? new Map() : undefined,
@@ -218,8 +242,18 @@ export class XLinkSDK {
return checkingResult.some(r => r) return checkingResult.some(r => r)
} }
stacksAddressFromStacksToken = stacksAddressFromStacksToken stacksAddressFromStacksToken(
stacksAddressToStacksToken = stacksAddressToStacksToken chain: ChainId,
token: KnownTokenId.StacksToken,
): Promise<undefined | StacksContractAddress> {
return stacksAddressFromStacksToken(this.sdkContext, chain, token)
}
stacksAddressToStacksToken(
chain: ChainId,
address: StacksContractAddress,
): Promise<undefined | KnownTokenId.StacksToken> {
return stacksAddressToStacksToken(this.sdkContext, chain, address)
}
/** /**
* This function provides detailed information about token transfers from the Stacks network to other supported * This function provides detailed information about token transfers from the Stacks network to other supported
@@ -296,6 +330,18 @@ export class XLinkSDK {
return return
} }
async evmChainIdFromKnownChainId(
chain: KnownChainId.EVMChain,
): Promise<undefined | bigint> {
return evmChainIdFromKnownChainId(chain)
}
async evmChainIdToKnownChainId(
chainId: bigint,
): Promise<undefined | KnownChainId.EVMChain> {
return evmChainIdToKnownChainId(chainId)
}
/** /**
* This function retrieves the contract address of a specific token on a given EVM-compatible blockchain. * 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. * @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. * or `undefined` if the chain is not a Stacks chain or if the contract address cannot be retrieved.
*/ */
async function stacksAddressFromStacksToken( async function stacksAddressFromStacksToken(
sdkContext: SDKGlobalContext,
chain: ChainId, chain: ChainId,
token: KnownTokenId.StacksToken, token: KnownTokenId.StacksToken,
): Promise<undefined | StacksContractAddress> { ): Promise<undefined | StacksContractAddress> {
if (!KnownChainId.isStacksChain(chain)) return if (!KnownChainId.isStacksChain(chain)) return
const info = await getStacksTokenContractInfo(chain, token) const info = await getStacksTokenContractInfo(sdkContext, chain, token)
if (info == null) return if (info == null) return
return { return {
deployerAddress: info.deployerAddress, deployerAddress: info.deployerAddress,
@@ -604,11 +651,12 @@ async function stacksAddressFromStacksToken(
* cannot be found. * cannot be found.
*/ */
async function stacksAddressToStacksToken( async function stacksAddressToStacksToken(
sdkContext: SDKGlobalContext,
chain: ChainId, chain: ChainId,
address: StacksContractAddress, address: StacksContractAddress,
): Promise<undefined | KnownTokenId.StacksToken> { ): Promise<undefined | KnownTokenId.StacksToken> {
if (!KnownChainId.isStacksChain(chain)) return if (!KnownChainId.isStacksChain(chain)) return
return getStacksToken(chain, address) return getStacksToken(sdkContext, chain, address)
} }
async function brc20TickFromBRC20Token( async function brc20TickFromBRC20Token(
@@ -627,7 +675,8 @@ async function brc20TickToBRC20Token(
): Promise<undefined | KnownTokenId.BRC20Token> { ): Promise<undefined | KnownTokenId.BRC20Token> {
if (!KnownChainId.isBRC20Chain(chain)) return if (!KnownChainId.isBRC20Chain(chain)) return
const routes = await getBRC20SupportedRoutes(sdkContext, chain) 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( async function runesIdFromRunesToken(

View File

@@ -15,9 +15,17 @@ export async function createBitcoinPegInRecipients(
sdkContext: Pick<SDKGlobalContext, "backendAPI">, sdkContext: Pick<SDKGlobalContext, "backendAPI">,
info: { info: {
fromChain: KnownChainId.BitcoinChain fromChain: KnownChainId.BitcoinChain
toChain: KnownChainId.StacksChain | KnownChainId.EVMChain
fromToken: KnownTokenId.BitcoinToken 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: { fromAddress: {
address: string address: string
scriptPubKey: Uint8Array scriptPubKey: Uint8Array

View File

@@ -1,8 +1,13 @@
import { callReadOnlyFunction } from "@stacks/transactions" import { evmTokenFromCorrespondingStacksToken } from "../evmUtils/peggingHelpers"
import { CallReadOnlyFunctionFn } from "clarity-codegen"
import { fromCorrespondingStacksToken } from "../evmUtils/peggingHelpers"
import { getEVMTokenContractInfo } from "../evmUtils/xlinkContractHelpers" import { getEVMTokenContractInfo } from "../evmUtils/xlinkContractHelpers"
import { stxTokenContractAddresses } from "../stacksUtils/stxContractAddresses" import {
getBRC20SupportedRoutes,
getRunesSupportedRoutes,
} from "../metaUtils/xlinkContractHelpers"
import {
StacksContractName,
stxTokenContractAddresses,
} from "../stacksUtils/stxContractAddresses"
import { import {
executeReadonlyCallXLINK, executeReadonlyCallXLINK,
getStacksContractCallInfo, getStacksContractCallInfo,
@@ -13,53 +18,66 @@ import {
IsSupportedFn, IsSupportedFn,
KnownRoute_FromBitcoin_ToStacks, KnownRoute_FromBitcoin_ToStacks,
KnownRoute_FromStacks_ToBitcoin, KnownRoute_FromStacks_ToBitcoin,
KnownRoute_ToStacks,
} from "../utils/buildSupportedRoutes" } from "../utils/buildSupportedRoutes"
import { props } from "../utils/promiseHelpers" import { props } from "../utils/promiseHelpers"
import { checkNever } from "../utils/typeHelpers" import {
import { TransferProphet } from "../utils/types/TransferProphet" getFinalStepStacksTokenAddress,
SwapRoute,
} from "../utils/SwapRouteHelpers"
import { assertExclude, checkNever } from "../utils/typeHelpers"
import { import {
_allNoLongerSupportedEVMChains, _allNoLongerSupportedEVMChains,
KnownChainId, KnownChainId,
KnownTokenId, KnownTokenId,
} from "../utils/types/knownIds" } from "../utils/types/knownIds"
import { TransferProphet } from "../utils/types/TransferProphet"
import { getBTCPegInAddress } from "./btcAddresses" import { getBTCPegInAddress } from "./btcAddresses"
export const getBtc2StacksFeeInfo = async ( export const getBtc2StacksFeeInfo = async (
route: KnownRoute_FromBitcoin_ToStacks, route: KnownRoute_FromBitcoin_ToStacks,
options: {
swapRoute: null | SwapRoute
},
): Promise<undefined | TransferProphet> => { ): Promise<undefined | TransferProphet> => {
const stacksContractCallInfo = getStacksContractCallInfo( const stacksBaseContractCallInfo = getStacksContractCallInfo(
route.toChain, route.toChain,
"btc-peg-in-endpoint-v2-05", StacksContractName.BTCPegInEndpoint,
) )
if (stacksContractCallInfo == null) return const stacksSwapContractCallInfo = getStacksContractCallInfo(
route.toChain,
const executeOptions = { StacksContractName.BTCPegInEndpointSwap,
deployerAddress: stacksContractCallInfo.deployerAddress, )
callReadOnlyFunction: (callOptions => if (
callReadOnlyFunction({ stacksBaseContractCallInfo == null ||
...callOptions, stacksSwapContractCallInfo == null
network: stacksContractCallInfo.network, ) {
})) satisfies CallReadOnlyFunctionFn, return
} }
const contractCallInfo =
options.swapRoute == null
? stacksBaseContractCallInfo
: stacksSwapContractCallInfo
const resp = await props({ const resp = await props({
isPaused: executeReadonlyCallXLINK( isPaused: executeReadonlyCallXLINK(
stacksContractCallInfo.contractName, contractCallInfo.contractName,
"is-peg-in-paused", "is-peg-in-paused",
{}, {},
executeOptions, contractCallInfo.executeOptions,
), ),
feeRate: executeReadonlyCallXLINK( feeRate: executeReadonlyCallXLINK(
stacksContractCallInfo.contractName, contractCallInfo.contractName,
"get-peg-in-fee", "get-peg-in-fee",
{}, {},
executeOptions, contractCallInfo.executeOptions,
).then(numberFromStacksContractNumber), ).then(numberFromStacksContractNumber),
minFeeAmount: executeReadonlyCallXLINK( minFeeAmount: executeReadonlyCallXLINK(
stacksContractCallInfo.contractName, contractCallInfo.contractName,
"get-peg-in-min-fee", "get-peg-in-min-fee",
{}, {},
executeOptions, contractCallInfo.executeOptions,
).then(numberFromStacksContractNumber), ).then(numberFromStacksContractNumber),
}) })
@@ -83,41 +101,103 @@ export const getBtc2StacksFeeInfo = async (
export const getStacks2BtcFeeInfo = async ( export const getStacks2BtcFeeInfo = async (
route: KnownRoute_FromStacks_ToBitcoin, route: KnownRoute_FromStacks_ToBitcoin,
options: {
swappedFromRoute: null | KnownRoute_ToStacks
},
): Promise<undefined | TransferProphet> => { ): Promise<undefined | TransferProphet> => {
const stacksContractCallInfo = getStacksContractCallInfo( const stacksContractCallInfo = getStacksContractCallInfo(
route.fromChain, 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 = { let feeInfo:
deployerAddress: stacksContractCallInfo.deployerAddress, | undefined
callReadOnlyFunction: (callOptions => | {
callReadOnlyFunction({ feeRate: Promise<BigNumber>
...callOptions, minFeeAmount: Promise<BigNumber>
network: stacksContractCallInfo.network, }
})) satisfies CallReadOnlyFunctionFn, 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<KnownChainId.EVMChain>(),
)
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({ const resp = await props({
...feeInfo,
isPaused: executeReadonlyCallXLINK( isPaused: executeReadonlyCallXLINK(
stacksContractCallInfo.contractName, stacksContractCallInfo.contractName,
"is-peg-out-paused", "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 { return {
@@ -172,21 +252,25 @@ export const isSupportedBitcoinRoute: IsSupportedFn = async (ctx, route) => {
if (KnownChainId.isBitcoinChain(toChain)) { if (KnownChainId.isBitcoinChain(toChain)) {
return false return false
} }
if (KnownChainId.isRunesChain(toChain)) {
return false const finalStepStacksToken =
} route.swapRoute == null
if (KnownChainId.isBRC20Chain(toChain)) { ? KnownTokenId.Stacks.aBTC
return false : await getFinalStepStacksTokenAddress(ctx, {
} swap: route.swapRoute,
stacksChain:
fromChain === KnownChainId.Bitcoin.Mainnet
? KnownChainId.Stacks.Mainnet
: KnownChainId.Stacks.Testnet,
})
if (KnownChainId.isStacksChain(toChain)) { if (KnownChainId.isStacksChain(toChain)) {
if (!KnownTokenId.isStacksToken(toToken)) return false if (!KnownTokenId.isStacksToken(toToken)) return false
return ( if (fromToken !== KnownTokenId.Bitcoin.BTC) return false
fromToken === KnownTokenId.Bitcoin.BTC && if (stxTokenContractAddresses[toToken]?.[toChain] == null) return false
toToken === KnownTokenId.Stacks.aBTC &&
stxTokenContractAddresses[toToken]?.[toChain] != null return toToken === finalStepStacksToken
)
} }
if (KnownChainId.isEVMChain(toChain)) { if (KnownChainId.isEVMChain(toChain)) {
@@ -195,11 +279,30 @@ export const isSupportedBitcoinRoute: IsSupportedFn = async (ctx, route) => {
const info = await getEVMTokenContractInfo(ctx, toChain, toToken) const info = await getEVMTokenContractInfo(ctx, toChain, toToken)
if (info == null) return false if (info == null) return false
const toEVMTokens = await fromCorrespondingStacksToken( if (finalStepStacksToken == null) return false
return evmTokenFromCorrespondingStacksToken(
toChain, 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) checkNever(toChain)

View File

@@ -8,3 +8,17 @@ export const STACKS_MAINNET = new StacksMainnet({
export const STACKS_TESTNET = new StacksMocknet({ export const STACKS_TESTNET = new StacksMocknet({
url: "https://nakamoto-dev-api.alexlab.co", url: "https://nakamoto-dev-api.alexlab.co",
}) })
export const envName = process.env.ENV_NAME === "dev" ? "dev" : "prod"
export const contractNameOverrides: Record<string, string> =
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",
}

View File

@@ -25,129 +25,146 @@ import {
xLayer, xLayer,
} from "./evmChainInfos" } from "./evmChainInfos"
import { EVMChain } from "./evmContractAddresses" import { EVMChain } from "./evmContractAddresses"
import { KnownChainId } from "../utils/types/knownIds"
import { entries } from "../utils/objectHelper"
export const defaultEvmClients: Record< export const defaultEvmClients: Partial<Record<KnownChainId.EVMChain, Client>> =
Exclude<EVMChain, typeof EVMChain.BitboyTestnet>, {
Client [EVMChain.Ethereum]: createClient({
> = { chain: mainnet,
[EVMChain.Ethereum]: createClient({ transport: http(),
chain: mainnet, batch: { multicall: true },
transport: http(), }),
batch: { multicall: true }, [EVMChain.Sepolia]: createClient({
}), chain: sepolia,
[EVMChain.Sepolia]: createClient({ transport: http(),
chain: sepolia, batch: { multicall: true },
transport: http(), }),
batch: { multicall: true },
}),
[EVMChain.BSC]: createClient({ [EVMChain.BSC]: createClient({
chain: bsc, chain: bsc,
transport: http(), transport: http(),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.BSCTestnet]: createClient({ [EVMChain.BSCTestnet]: createClient({
chain: bscTestnet, chain: bscTestnet,
transport: http(), transport: http(),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.CoreDAO]: createClient({ [EVMChain.CoreDAO]: createClient({
chain: coreDao, chain: coreDao,
transport: http(), transport: http(),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.CoreDAOTestnet]: createClient({ [EVMChain.CoreDAOTestnet]: createClient({
chain: coreDaoTestnet, chain: coreDaoTestnet,
transport: http(), transport: http(),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.Bsquared]: createClient({ [EVMChain.Bsquared]: createClient({
chain: bsquared, chain: bsquared,
transport: http(), transport: http(),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.BOB]: createClient({ [EVMChain.BOB]: createClient({
chain: bob, chain: bob,
transport: http(), transport: http(),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.Bitlayer]: createClient({ [EVMChain.Bitlayer]: createClient({
chain: bitlayer, chain: bitlayer,
transport: http(), transport: http(),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.Lorenzo]: createClient({ [EVMChain.Lorenzo]: createClient({
chain: lorenzo, chain: lorenzo,
transport: http(), transport: http(),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.Merlin]: createClient({ [EVMChain.Merlin]: createClient({
chain: merlin, chain: merlin,
transport: http(), transport: http(),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.AILayer]: createClient({ [EVMChain.AILayer]: createClient({
chain: ailayer, chain: ailayer,
transport: http(), transport: http(),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.Mode]: createClient({ [EVMChain.Mode]: createClient({
chain: mode, chain: mode,
transport: http(), transport: http(),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.XLayer]: createClient({ [EVMChain.XLayer]: createClient({
chain: xLayer, chain: xLayer,
transport: fallback([http(), http("https://xlayerrpc.okx.com")]), transport: fallback([http(), http("https://xlayerrpc.okx.com")]),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.Arbitrum]: createClient({ [EVMChain.Arbitrum]: createClient({
chain: arbitrum, chain: arbitrum,
transport: http(), transport: http(),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.Aurora]: createClient({ [EVMChain.Aurora]: createClient({
chain: aurora, chain: aurora,
transport: http(), transport: http(),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.Manta]: createClient({ [EVMChain.Manta]: createClient({
chain: manta, chain: manta,
transport: http(), transport: http(),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.Linea]: createClient({ [EVMChain.Linea]: createClient({
chain: linea, chain: linea,
transport: http(), transport: http(),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.Base]: createClient({ [EVMChain.Base]: createClient({
chain: base, chain: base,
transport: http(), transport: http(),
batch: { multicall: true }, batch: { multicall: true },
}), }),
[EVMChain.BlifeTestnet]: createClient({ [EVMChain.BlifeTestnet]: createClient({
chain: blifeTestnet, chain: blifeTestnet,
transport: http(), transport: http(),
}), }),
[EVMChain.BeraTestnet]: createClient({ [EVMChain.BeraTestnet]: createClient({
chain: berachainTestnet, chain: berachainTestnet,
transport: http(), transport: http(),
}), batch: { multicall: true },
}),
} satisfies Record<Exclude<EVMChain, typeof EVMChain.BitboyTestnet>, 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]
} }

View File

@@ -1,5 +1,4 @@
import { callReadOnlyFunction } from "@stacks/transactions" import { unwrapResponse } from "clarity-codegen"
import { CallReadOnlyFunctionFn, unwrapResponse } from "clarity-codegen"
import { readContract } from "viem/actions" import { readContract } from "viem/actions"
import { import {
getBRC20SupportedRoutes, getBRC20SupportedRoutes,
@@ -8,6 +7,7 @@ import {
import { contractAssignedChainIdFromKnownChain } from "../stacksUtils/crossContractDataMapping" import { contractAssignedChainIdFromKnownChain } from "../stacksUtils/crossContractDataMapping"
import { import {
getTerminatingStacksTokenContractAddress, getTerminatingStacksTokenContractAddress,
StacksContractName,
stxTokenContractAddresses, stxTokenContractAddresses,
} from "../stacksUtils/stxContractAddresses" } from "../stacksUtils/stxContractAddresses"
import { import {
@@ -46,7 +46,7 @@ export const getEvm2StacksFeeInfo = async (
): Promise<undefined | TransferProphet> => { ): Promise<undefined | TransferProphet> => {
const stacksContractCallInfo = getStacksContractCallInfo( const stacksContractCallInfo = getStacksContractCallInfo(
route.toChain, route.toChain,
"cross-peg-in-endpoint-v2-04", StacksContractName.EVMPegInEndpoint,
) )
const evmContractCallInfo = await getEVMContractCallInfo(ctx, route.fromChain) const evmContractCallInfo = await getEVMContractCallInfo(ctx, route.fromChain)
const evmTokenContractCallInfo = await getEVMTokenContractInfo( const evmTokenContractCallInfo = await getEVMTokenContractInfo(
@@ -66,15 +66,6 @@ export const getEvm2StacksFeeInfo = async (
return getEvm2StacksNativeBridgeFeeInfo(ctx, route) return getEvm2StacksNativeBridgeFeeInfo(ctx, route)
} }
const executeOptions = {
deployerAddress: stacksContractCallInfo.deployerAddress,
callReadOnlyFunction: (callOptions =>
callReadOnlyFunction({
...callOptions,
network: stacksContractCallInfo.network,
})) satisfies CallReadOnlyFunctionFn,
}
const { client, tokenContractAddress } = evmTokenContractCallInfo const { client, tokenContractAddress } = evmTokenContractCallInfo
const registryAddr = const registryAddr =
@@ -126,7 +117,7 @@ export const getEvm2StacksFeeInfo = async (
stacksContractCallInfo.contractName, stacksContractCallInfo.contractName,
"get-paused", "get-paused",
{}, {},
executeOptions, stacksContractCallInfo.executeOptions,
), ),
}) })
@@ -157,7 +148,7 @@ const getEvm2StacksNativeBridgeFeeInfo = async (
): Promise<undefined | TransferProphet> => { ): Promise<undefined | TransferProphet> => {
const stacksContractCallInfo = getStacksContractCallInfo( const stacksContractCallInfo = getStacksContractCallInfo(
route.toChain, route.toChain,
"cross-peg-in-endpoint-v2-04", StacksContractName.EVMPegInEndpoint,
) )
const evmContractCallInfo = await getEVMContractCallInfo(ctx, route.fromChain) const evmContractCallInfo = await getEVMContractCallInfo(ctx, route.fromChain)
if ( if (
@@ -167,21 +158,12 @@ const getEvm2StacksNativeBridgeFeeInfo = async (
return return
} }
const executeOptions = {
deployerAddress: stacksContractCallInfo.deployerAddress,
callReadOnlyFunction: (callOptions =>
callReadOnlyFunction({
...callOptions,
network: stacksContractCallInfo.network,
})) satisfies CallReadOnlyFunctionFn,
}
const resp = await props({ const resp = await props({
isPaused: executeReadonlyCallXLINK( isPaused: executeReadonlyCallXLINK(
stacksContractCallInfo.contractName, stacksContractCallInfo.contractName,
"get-paused", "get-paused",
{}, {},
executeOptions, stacksContractCallInfo.executeOptions,
), ),
}) })
@@ -195,13 +177,15 @@ const getEvm2StacksNativeBridgeFeeInfo = async (
} }
export const getStacks2EvmFeeInfo = async ( export const getStacks2EvmFeeInfo = async (
ctx: SDKGlobalContext,
route: KnownRoute_FromStacks_ToEVM, route: KnownRoute_FromStacks_ToEVM,
): Promise<undefined | TransferProphet> => { ): Promise<undefined | TransferProphet> => {
const stacksContractCallInfo = getStacksContractCallInfo( const stacksContractCallInfo = getStacksContractCallInfo(
route.fromChain, route.fromChain,
"cross-peg-out-endpoint-v2-01", StacksContractName.EVMPegOutEndpoint,
) )
const stacksTokenContractCallInfo = getStacksTokenContractInfo( const stacksTokenContractCallInfo = await getStacksTokenContractInfo(
ctx,
route.fromChain, route.fromChain,
route.fromToken, route.fromToken,
) )
@@ -214,15 +198,6 @@ export const getStacks2EvmFeeInfo = async (
getTerminatingStacksTokenContractAddress(route) ?? getTerminatingStacksTokenContractAddress(route) ??
stacksTokenContractCallInfo stacksTokenContractCallInfo
const executeOptions = {
deployerAddress: stacksContractCallInfo.deployerAddress,
callReadOnlyFunction: (callOptions =>
callReadOnlyFunction({
...callOptions,
network: stacksContractCallInfo.network,
})) satisfies CallReadOnlyFunctionFn,
}
const tokenConf = await Promise.all([ const tokenConf = await Promise.all([
executeReadonlyCallXLINK( executeReadonlyCallXLINK(
stacksContractCallInfo.contractName, stacksContractCallInfo.contractName,
@@ -233,13 +208,13 @@ export const getStacks2EvmFeeInfo = async (
"chain-id": toChainId, "chain-id": toChainId,
}, },
}, },
executeOptions, stacksContractCallInfo.executeOptions,
), ),
executeReadonlyCallXLINK( executeReadonlyCallXLINK(
stacksContractCallInfo.contractName, stacksContractCallInfo.contractName,
"get-paused", "get-paused",
{}, {},
executeOptions, stacksContractCallInfo.executeOptions,
), ),
]).then(([resp, isPaused]) => { ]).then(([resp, isPaused]) => {
if (resp.type !== "success") return undefined 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, toChain: KnownChainId.EVMChain,
stacksToken: KnownTokenId.StacksToken, stacksToken: KnownTokenId.StacksToken,
): Promise<KnownTokenId.EVMToken[]> { ): Promise<KnownTokenId.EVMToken[]> {
@@ -348,7 +323,7 @@ export async function fromCorrespondingStacksToken(
checkNever(restEVMTokenPossibilities) checkNever(restEVMTokenPossibilities)
return [] return []
} }
export async function toCorrespondingStacksToken( export async function evmTokenToCorrespondingStacksToken(
evmToken: KnownTokenId.EVMToken, evmToken: KnownTokenId.EVMToken,
): Promise<undefined | KnownTokenId.StacksToken> { ): Promise<undefined | KnownTokenId.StacksToken> {
const EVMToken = KnownTokenId.EVM const EVMToken = KnownTokenId.EVM
@@ -429,7 +404,7 @@ export const isSupportedEVMRoute: IsSupportedFn = async (ctx, route) => {
if (KnownChainId.isStacksChain(toChain)) { if (KnownChainId.isStacksChain(toChain)) {
if (!KnownTokenId.isStacksToken(toToken)) return false if (!KnownTokenId.isStacksToken(toToken)) return false
const stacksToken = await toCorrespondingStacksToken(fromToken) const stacksToken = await evmTokenToCorrespondingStacksToken(fromToken)
if (stacksToken == null) return false if (stacksToken == null) return false
if (stxTokenContractAddresses[stacksToken]?.[toChain] == null) { if (stxTokenContractAddresses[stacksToken]?.[toChain] == null) {
@@ -445,10 +420,11 @@ export const isSupportedEVMRoute: IsSupportedFn = async (ctx, route) => {
const toTokenInfo = await getEVMTokenContractInfo(ctx, toChain, toToken) const toTokenInfo = await getEVMTokenContractInfo(ctx, toChain, toToken)
if (toTokenInfo == null) return false if (toTokenInfo == null) return false
const transitStacksToken = await toCorrespondingStacksToken(fromToken) const transitStacksToken =
await evmTokenToCorrespondingStacksToken(fromToken)
if (transitStacksToken == null) return false if (transitStacksToken == null) return false
const toEVMTokens = await fromCorrespondingStacksToken( const toEVMTokens = await evmTokenFromCorrespondingStacksToken(
toChain, toChain,
transitStacksToken, transitStacksToken,
) )
@@ -457,14 +433,15 @@ export const isSupportedEVMRoute: IsSupportedFn = async (ctx, route) => {
if (KnownChainId.isBitcoinChain(toChain)) { if (KnownChainId.isBitcoinChain(toChain)) {
if (!KnownTokenId.isBitcoinToken(toToken)) return false if (!KnownTokenId.isBitcoinToken(toToken)) return false
const stacksToken = await toCorrespondingStacksToken(fromToken) const stacksToken = await evmTokenToCorrespondingStacksToken(fromToken)
return stacksToken === KnownTokenId.Stacks.aBTC return stacksToken === KnownTokenId.Stacks.aBTC
} }
if (KnownChainId.isRunesChain(toChain)) { if (KnownChainId.isRunesChain(toChain)) {
if (!KnownTokenId.isRunesToken(toToken)) return false if (!KnownTokenId.isRunesToken(toToken)) return false
const transitStacksToken = await toCorrespondingStacksToken(fromToken) const transitStacksToken =
await evmTokenToCorrespondingStacksToken(fromToken)
if (transitStacksToken == null) return false if (transitStacksToken == null) return false
const runesRoutes = await getRunesSupportedRoutes(ctx, toChain) const runesRoutes = await getRunesSupportedRoutes(ctx, toChain)
@@ -474,7 +451,8 @@ export const isSupportedEVMRoute: IsSupportedFn = async (ctx, route) => {
if (KnownChainId.isBRC20Chain(toChain)) { if (KnownChainId.isBRC20Chain(toChain)) {
if (!KnownTokenId.isBRC20Token(toToken)) return false if (!KnownTokenId.isBRC20Token(toToken)) return false
const transitStacksToken = await toCorrespondingStacksToken(fromToken) const transitStacksToken =
await evmTokenToCorrespondingStacksToken(fromToken)
if (transitStacksToken == null) return false if (transitStacksToken == null) return false
const brc20Routes = await getBRC20SupportedRoutes(ctx, toChain) const brc20Routes = await getBRC20SupportedRoutes(ctx, toChain)

View File

@@ -156,7 +156,7 @@ const getOnChainConfigs = async (
configContractAddress: Address, configContractAddress: Address,
): Promise<undefined | EVMOnChainAddresses> => { ): Promise<undefined | EVMOnChainAddresses> => {
const cache = sdkContext.evm.onChainConfigCache const cache = sdkContext.evm.onChainConfigCache
const cacheKey = `${chain}:${configContractAddress}` const cacheKey = `${chain}:${configContractAddress}` as const
if (cache != null) { if (cache != null) {
const cachedPromise = cache.get(cacheKey) const cachedPromise = cache.get(cacheKey)

View File

@@ -12,6 +12,11 @@ export {
StacksContractAddress, StacksContractAddress,
PublicEVMContractType as EVMContractType, PublicEVMContractType as EVMContractType,
} from "./xlinkSdkUtils/types" } 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 { TimeLockedAsset } from "./xlinkSdkUtils/timelockFromEVM"
export { export {
PublicTransferProphet as TransferProphet, PublicTransferProphet as TransferProphet,

View File

@@ -4,9 +4,13 @@
*/ */
import { import {
fromCorrespondingStacksToken, evmTokenFromCorrespondingStacksToken,
toCorrespondingStacksToken, evmTokenToCorrespondingStacksToken,
} from "./evmUtils/peggingHelpers" } from "./evmUtils/peggingHelpers"
import {
metaTokenFromCorrespondingStacksToken,
metaTokenToCorrespondingStacksToken,
} from "./metaUtils/peggingHelpers"
import { KnownChainId, KnownTokenId } from "./utils/types/knownIds" import { KnownChainId, KnownTokenId } from "./utils/types/knownIds"
import { SDKGlobalContext } from "./xlinkSdkUtils/types.internal" import { SDKGlobalContext } from "./xlinkSdkUtils/types.internal"
@@ -16,22 +20,31 @@ export {
} from "./stacksUtils/crossContractDataMapping" } from "./stacksUtils/crossContractDataMapping"
export { export {
fromCorrespondingStacksToken as evmTokensFromCorrespondingStacksToken,
toCorrespondingStacksToken as evmTokenToCorrespondingStacksToken,
}
export {
getTerminatingStacksTokenContractAddress,
getEVMTokenIdFromTerminatingStacksTokenContractAddress, getEVMTokenIdFromTerminatingStacksTokenContractAddress,
getTerminatingStacksTokenContractAddress,
} from "./stacksUtils/stxContractAddresses" } from "./stacksUtils/stxContractAddresses"
export {
addressFromBuffer,
addressToBuffer,
} from "./stacksUtils/xlinkContractHelpers"
export { isSupportedMetaRoute } from "./metaUtils/peggingHelpers" 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 { bridgeFromEVM_toLaunchpad } from "./xlinkSdkUtils/bridgeFromEVM"
export const getXLinkSDKContext = ( export const getXLinkSDKContext = (
@@ -46,7 +59,7 @@ export const evmTokensFromStacksToken = async (options: {
}): Promise<{ }): Promise<{
evmTokens: KnownTokenId.EVMToken[] evmTokens: KnownTokenId.EVMToken[]
}> => { }> => {
const evmTokens = await fromCorrespondingStacksToken( const evmTokens = await evmTokenFromCorrespondingStacksToken(
options.toEVMChain, options.toEVMChain,
options.fromStacksToken, options.fromStacksToken,
) )
@@ -58,6 +71,44 @@ export const evmTokenToStacksToken = async (options: {
}): Promise<{ }): Promise<{
stacksTokens: KnownTokenId.StacksToken[] 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] } return { stacksTokens: stacksTokens == null ? [] : [stacksTokens] }
} }

View File

@@ -1,13 +1,25 @@
import { toCorrespondingStacksToken } from "../evmUtils/peggingHelpers" import { evmTokenToCorrespondingStacksToken } from "../evmUtils/peggingHelpers"
import { getEVMTokenContractInfo } from "../evmUtils/xlinkContractHelpers" import { getEVMTokenContractInfo } from "../evmUtils/xlinkContractHelpers"
import { StacksContractName } from "../stacksUtils/stxContractAddresses"
import {
executeReadonlyCallXLINK,
getStacksContractCallInfo,
numberFromStacksContractNumber,
} from "../stacksUtils/xlinkContractHelpers"
import { BigNumber } from "../utils/BigNumber" import { BigNumber } from "../utils/BigNumber"
import { import {
_KnownRoute_FromBRC20_ToStacks, getFinalStepStacksTokenAddress,
_KnownRoute_FromRunes_ToStacks, getFirstStepStacksTokenAddress,
SwapRoute,
} from "../utils/SwapRouteHelpers"
import {
IsSupportedFn, IsSupportedFn,
KnownRoute_FromBRC20_ToStacks,
KnownRoute_FromRunes_ToStacks,
KnownRoute_FromStacks_ToBRC20, KnownRoute_FromStacks_ToBRC20,
KnownRoute_FromStacks_ToRunes, KnownRoute_FromStacks_ToRunes,
} from "../utils/buildSupportedRoutes" } from "../utils/buildSupportedRoutes"
import { props } from "../utils/promiseHelpers"
import { checkNever, isNotNull } from "../utils/typeHelpers" import { checkNever, isNotNull } from "../utils/typeHelpers"
import { TransferProphet } from "../utils/types/TransferProphet" import { TransferProphet } from "../utils/types/TransferProphet"
import { import {
@@ -18,15 +30,62 @@ import {
import { SDKGlobalContext } from "../xlinkSdkUtils/types.internal" import { SDKGlobalContext } from "../xlinkSdkUtils/types.internal"
import { getMetaPegInAddress } from "./btcAddresses" import { getMetaPegInAddress } from "./btcAddresses"
import { import {
BRC20SupportedRoute,
getBRC20SupportedRoutes, getBRC20SupportedRoutes,
getRunesSupportedRoutes, getRunesSupportedRoutes,
RunesSupportedRoute,
} from "./xlinkContractHelpers" } from "./xlinkContractHelpers"
export async function metaTokenFromCorrespondingStacksToken(
ctx: SDKGlobalContext,
chain: KnownChainId.BRC20Chain | KnownChainId.RunesChain,
stacksToken: KnownTokenId.StacksToken,
): Promise<undefined | KnownTokenId.BRC20Token | KnownTokenId.RunesToken> {
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<undefined | KnownTokenId.StacksToken> {
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 ( export const getMeta2StacksFeeInfo = async (
ctx: SDKGlobalContext, ctx: SDKGlobalContext,
route: _KnownRoute_FromBRC20_ToStacks | _KnownRoute_FromRunes_ToStacks, route: KnownRoute_FromBRC20_ToStacks | KnownRoute_FromRunes_ToStacks,
options: {
swapRoute: null | SwapRoute
},
): Promise<undefined | TransferProphet> => {
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<undefined | TransferProphet> => { ): Promise<undefined | TransferProphet> => {
const filteredRoutes = KnownChainId.isBRC20Chain(route.fromChain) const filteredRoutes = KnownChainId.isBRC20Chain(route.fromChain)
? await getBRC20SupportedRoutes(ctx, route.fromChain).then(routes => ? await getBRC20SupportedRoutes(ctx, route.fromChain).then(routes =>
@@ -60,7 +119,7 @@ export const getMeta2StacksFeeInfo = async (
? null ? null
: { : {
type: "fixed" as const, type: "fixed" as const,
token: KnownTokenId.Stacks.aBTC, token: KnownTokenId.Bitcoin.BTC,
amount: filteredRoute.pegInFeeBitcoinAmount, amount: filteredRoute.pegInFeeBitcoinAmount,
}, },
].filter(isNotNull), ].filter(isNotNull),
@@ -69,6 +128,47 @@ export const getMeta2StacksFeeInfo = async (
} }
} }
const getMeta2StacksSwapFeeInfo = async (
route1: KnownRoute_FromBRC20_ToStacks | KnownRoute_FromRunes_ToStacks,
): Promise<undefined | TransferProphet> => {
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 ( export const getStacks2MetaFeeInfo = async (
ctx: SDKGlobalContext, ctx: SDKGlobalContext,
route: KnownRoute_FromStacks_ToBRC20 | KnownRoute_FromStacks_ToRunes, route: KnownRoute_FromStacks_ToBRC20 | KnownRoute_FromStacks_ToRunes,
@@ -105,7 +205,7 @@ export const getStacks2MetaFeeInfo = async (
? null ? null
: { : {
type: "fixed" as const, type: "fixed" as const,
token: KnownTokenId.Stacks.aBTC, token: KnownTokenId.Bitcoin.BTC,
amount: filteredRoute.pegOutFeeBitcoinAmount, amount: filteredRoute.pegOutFeeBitcoinAmount,
}, },
].filter(isNotNull), ].filter(isNotNull),
@@ -158,13 +258,23 @@ export const isSupportedMetaRoute: IsSupportedFn = async (ctx, route) => {
if (!KnownTokenId.isStacksToken(toToken)) return false if (!KnownTokenId.isStacksToken(toToken)) return false
if (KnownChainId.isRunesChain(fromChain)) { if (KnownChainId.isRunesChain(fromChain)) {
if (!KnownTokenId.isRunesToken(fromToken)) return false
const runesRoutes = await getRunesSupportedRoutes(ctx, fromChain) 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 (KnownChainId.isBRC20Chain(fromChain)) {
if (!KnownTokenId.isBRC20Token(fromToken)) return false
const brc20Routes = await getBRC20SupportedRoutes(ctx, fromChain) 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) checkNever(fromChain)
@@ -177,17 +287,29 @@ export const isSupportedMetaRoute: IsSupportedFn = async (ctx, route) => {
const info = await getEVMTokenContractInfo(ctx, toChain, toToken) const info = await getEVMTokenContractInfo(ctx, toChain, toToken)
if (info == null) return false if (info == null) return false
const transitStacksToken = await toCorrespondingStacksToken(toToken) const transitStacksToken = await evmTokenToCorrespondingStacksToken(toToken)
if (transitStacksToken == null) return false if (transitStacksToken == null) return false
if (KnownChainId.isRunesChain(fromChain)) { if (KnownChainId.isRunesChain(fromChain)) {
if (!KnownTokenId.isRunesToken(fromToken)) return false
const runesRoutes = await getRunesSupportedRoutes(ctx, fromChain) 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 (KnownChainId.isBRC20Chain(fromChain)) {
if (!KnownTokenId.isBRC20Token(fromToken)) return false
const brc20Routes = await getBRC20SupportedRoutes(ctx, fromChain) 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) checkNever(fromChain)
@@ -197,39 +319,205 @@ export const isSupportedMetaRoute: IsSupportedFn = async (ctx, route) => {
if (KnownChainId.isRunesChain(toChain)) { if (KnownChainId.isRunesChain(toChain)) {
if (!KnownTokenId.isRunesToken(toToken)) return false if (!KnownTokenId.isRunesToken(toToken)) return false
// TODO: runes -> runes (swap) is not supported yet const finalStepStacksToken =
if (KnownChainId.isRunesChain(fromChain)) return false 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) // runes -> runes
const runesRoutes = await getRunesSupportedRoutes(ctx, toChain) if (KnownChainId.isRunesChain(fromChain)) {
return getRoutesOverlapping(brc20Routes, runesRoutes) != null 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 (KnownChainId.isBRC20Chain(toChain)) {
if (!KnownTokenId.isBRC20Token(toToken)) return false if (!KnownTokenId.isBRC20Token(toToken)) return false
// TODO: brc20 -> brc20 (swap) is not supported yet const finalStepStacksToken =
if (KnownChainId.isBRC20Chain(fromChain)) return false 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) // brc20 -> brc20
const runesRoutes = await getRunesSupportedRoutes(ctx, fromChain) if (KnownChainId.isBRC20Chain(fromChain)) {
return getRoutesOverlapping(brc20Routes, runesRoutes) != null 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) checkNever(toChain)
return false 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
}

View File

@@ -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<undefined | { token: KnownTokenId.BRC20Token }> => {
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<undefined | { tick: string }> => {
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<undefined | { token: KnownTokenId.RunesToken }> => {
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<undefined | { id: { blockHeight: bigint; txIndex: bigint } }> => {
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]) },
}
}

View File

@@ -43,7 +43,7 @@ export async function getBRC20SupportedRoutes(
return promise return promise
} }
async function _getBRC20SupportedRoutes( async function _getBRC20SupportedRoutes(
sdkContext: Pick<SDKGlobalContext, "backendAPI">, sdkContext: SDKGlobalContext,
chainId: KnownChainId.BRC20Chain, chainId: KnownChainId.BRC20Chain,
): Promise<BRC20SupportedRoute[]> { ): Promise<BRC20SupportedRoute[]> {
const stacksChainId = const stacksChainId =
@@ -65,6 +65,7 @@ async function _getBRC20SupportedRoutes(
const routes = await Promise.all( const routes = await Promise.all(
resp.routes.map(async (route): Promise<null | BRC20SupportedRoute> => { resp.routes.map(async (route): Promise<null | BRC20SupportedRoute> => {
const stacksToken = await getStacksToken( const stacksToken = await getStacksToken(
sdkContext,
stacksChainId, stacksChainId,
route.stacksTokenContractAddress, route.stacksTokenContractAddress,
) )
@@ -118,7 +119,6 @@ export interface RunesSupportedRoute {
pegOutFeeRate: BigNumber pegOutFeeRate: BigNumber
pegOutFeeBitcoinAmount: null | BigNumber pegOutFeeBitcoinAmount: null | BigNumber
} }
export async function getRunesSupportedRoutes( export async function getRunesSupportedRoutes(
sdkContext: SDKGlobalContext, sdkContext: SDKGlobalContext,
chainId: KnownChainId.RunesChain, chainId: KnownChainId.RunesChain,
@@ -141,7 +141,7 @@ export async function getRunesSupportedRoutes(
return promise return promise
} }
async function _getRunesSupportedRoutes( async function _getRunesSupportedRoutes(
sdkContext: Pick<SDKGlobalContext, "backendAPI">, sdkContext: SDKGlobalContext,
chainId: KnownChainId.RunesChain, chainId: KnownChainId.RunesChain,
): Promise<RunesSupportedRoute[]> { ): Promise<RunesSupportedRoute[]> {
const stacksChainId = const stacksChainId =
@@ -163,6 +163,7 @@ async function _getRunesSupportedRoutes(
const routes = await Promise.all( const routes = await Promise.all(
resp.routes.map(async (route): Promise<null | RunesSupportedRoute> => { resp.routes.map(async (route): Promise<null | RunesSupportedRoute> => {
const stacksToken = await getStacksToken( const stacksToken = await getStacksToken(
sdkContext,
stacksChainId, stacksChainId,
route.stacksTokenContractAddress, route.stacksTokenContractAddress,
) )

View File

@@ -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<undefined | CreateBridgeOrderResult> {
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<undefined | CreateBridgeOrderResult> {
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!,
}
}

View File

@@ -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<undefined | CreateBridgeOrderResult> {
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<KnownChainId.StacksChain>())
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<KnownChainId.EVMChain>())
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<KnownChainId.BRC20Chain>())
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<KnownChainId.RunesChain>())
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<undefined | CreateBridgeOrderResult> {
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<undefined | CreateBridgeOrderResult> {
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<undefined | CreateBridgeOrderResult> {
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!,
}
}

View File

@@ -1,4 +1,4 @@
import { fromCorrespondingStacksToken } from "../evmUtils/peggingHelpers" import { evmTokenFromCorrespondingStacksToken } from "../evmUtils/peggingHelpers"
import { getEVMTokenContractInfo } from "../evmUtils/xlinkContractHelpers" import { getEVMTokenContractInfo } from "../evmUtils/xlinkContractHelpers"
import { import {
getBRC20SupportedRoutes, getBRC20SupportedRoutes,
@@ -82,7 +82,10 @@ export const isSupportedStacksRoute: IsSupportedFn = async (ctx, route) => {
const info = await getEVMTokenContractInfo(ctx, toChain, toToken) const info = await getEVMTokenContractInfo(ctx, toChain, toToken)
if (info == null) return false if (info == null) return false
const toEVMTokens = await fromCorrespondingStacksToken(toChain, fromToken) const toEVMTokens = await evmTokenFromCorrespondingStacksToken(
toChain,
fromToken,
)
return toEVMTokens.includes(toToken) return toEVMTokens.includes(toToken)
} }

View File

@@ -1,3 +1,4 @@
import { contractNameOverrides } from "../config"
import { KnownRoute_FromStacks_ToEVM } from "../utils/buildSupportedRoutes" import { KnownRoute_FromStacks_ToEVM } from "../utils/buildSupportedRoutes"
import { assertExclude, checkNever } from "../utils/typeHelpers" import { assertExclude, checkNever } from "../utils/typeHelpers"
import { KnownChainId, KnownTokenId } from "../utils/types/knownIds" import { KnownChainId, KnownTokenId } from "../utils/types/knownIds"
@@ -21,51 +22,281 @@ export const alexContractDeployerMainnet =
export const alexContractDeployerTestnet = export const alexContractDeployerTestnet =
"ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK" "ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK"
export const alexContractMultisigMainnet =
"SP1E0XBN9T4B10E9QMR7XMFJPMA19D77WY3KP2QKC"
export const alexContractMultisigTestnet =
"ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK"
export const legacyAlexContractDeployerMainnet = export const legacyAlexContractDeployerMainnet =
"SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9" "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9"
export const legacyAlexContractDeployerTestnet = export const legacyAlexContractDeployerTestnet =
"ST1J2JTYXGRMZYNKE40GM87ZCACSPSSEEQVSNB7DC" "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<KnownChainId.StacksChain, StacksContractAddress>
>
export const stxTokenContractAddresses: Record<
string,
Record<KnownChainId.StacksChain, StacksContractAddress>
> = {
[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: { wbtc: {
[KnownChainId.Stacks.Mainnet]: { [KnownChainId.Stacks.Mainnet]: wrapContractAddress({
deployerAddress: xlinkContractsMultisigMainnet, deployerAddress: xlinkContractsMultisigMainnet,
contractName: "token-wbtc", contractName: "token-wbtc",
}, }),
[KnownChainId.Stacks.Testnet]: { [KnownChainId.Stacks.Testnet]: wrapContractAddress({
deployerAddress: xlinkContractsDeployerTestnet, deployerAddress: xlinkContractsDeployerTestnet,
contractName: "token-wbtc", contractName: "token-wbtc",
}, }),
}, },
btcb: { btcb: {
[KnownChainId.Stacks.Mainnet]: { [KnownChainId.Stacks.Mainnet]: wrapContractAddress({
deployerAddress: xlinkContractsMultisigMainnet, deployerAddress: xlinkContractsMultisigMainnet,
contractName: "token-wbtc", contractName: "token-wbtc",
}, }),
[KnownChainId.Stacks.Testnet]: { [KnownChainId.Stacks.Testnet]: wrapContractAddress({
deployerAddress: xlinkContractsDeployerTestnet, deployerAddress: xlinkContractsDeployerTestnet,
contractName: "token-btcb", contractName: "token-btcb",
}, }),
}, },
cbBTC: { cbBTC: {
[KnownChainId.Stacks.Mainnet]: { [KnownChainId.Stacks.Mainnet]: wrapContractAddress({
deployerAddress: xlinkContractsMultisigMainnet, deployerAddress: xlinkContractsMultisigMainnet,
contractName: "token-wbtc", contractName: "token-wbtc",
}, }),
[KnownChainId.Stacks.Testnet]: { [KnownChainId.Stacks.Testnet]: wrapContractAddress({
deployerAddress: xlinkContractsDeployerTestnet, deployerAddress: xlinkContractsDeployerTestnet,
contractName: "token-wbtc", contractName: "token-wbtc",
}, }),
}, },
usdt: { usdt: {
[KnownChainId.Stacks.Mainnet]: { [KnownChainId.Stacks.Mainnet]: wrapContractAddress({
deployerAddress: xlinkContractsMultisigMainnet, deployerAddress: xlinkContractsMultisigMainnet,
contractName: "token-usdt", contractName: "token-usdt",
}, }),
[KnownChainId.Stacks.Testnet]: { [KnownChainId.Stacks.Testnet]: wrapContractAddress({
deployerAddress: xlinkContractsDeployerTestnet, deployerAddress: xlinkContractsDeployerTestnet,
contractName: "token-usdt", contractName: "token-usdt",
}, }),
}, },
usdc: { usdc: {
[KnownChainId.Stacks.Mainnet]: { [KnownChainId.Stacks.Mainnet]: {
@@ -93,7 +324,7 @@ export const getTerminatingStacksTokenContractAddress = (
toChain === KnownChainId.EVM.Sepolia) && toChain === KnownChainId.EVM.Sepolia) &&
toToken === KnownTokenId.EVM.WBTC toToken === KnownTokenId.EVM.WBTC
) { ) {
return stxAlternativeTokenContractAddresses.wbtc[route.fromChain] return terminatingStacksTokenContractAddresses.wbtc[route.fromChain]
} }
assertExclude(restChains, KnownChainId.EVM.Ethereum) assertExclude(restChains, KnownChainId.EVM.Ethereum)
assertExclude(restChains, KnownChainId.EVM.Sepolia) assertExclude(restChains, KnownChainId.EVM.Sepolia)
@@ -103,7 +334,7 @@ export const getTerminatingStacksTokenContractAddress = (
toChain === KnownChainId.EVM.BSCTestnet) && toChain === KnownChainId.EVM.BSCTestnet) &&
toToken === KnownTokenId.EVM.BTCB toToken === KnownTokenId.EVM.BTCB
) { ) {
return stxAlternativeTokenContractAddresses.btcb[route.fromChain] return terminatingStacksTokenContractAddresses.btcb[route.fromChain]
} }
assertExclude(restChains, KnownChainId.EVM.BSC) assertExclude(restChains, KnownChainId.EVM.BSC)
assertExclude(restChains, KnownChainId.EVM.BSCTestnet) assertExclude(restChains, KnownChainId.EVM.BSCTestnet)
@@ -112,7 +343,7 @@ export const getTerminatingStacksTokenContractAddress = (
toChain === KnownChainId.EVM.Base && toChain === KnownChainId.EVM.Base &&
toToken === KnownTokenId.EVM.cbBTC toToken === KnownTokenId.EVM.cbBTC
) { ) {
return stxAlternativeTokenContractAddresses.cbBTC[route.fromChain] return terminatingStacksTokenContractAddresses.cbBTC[route.fromChain]
} }
assertExclude(restChains, KnownChainId.EVM.Base) assertExclude(restChains, KnownChainId.EVM.Base)
@@ -129,7 +360,7 @@ export const getTerminatingStacksTokenContractAddress = (
toChain === KnownChainId.EVM.BSCTestnet) && toChain === KnownChainId.EVM.BSCTestnet) &&
toToken === KnownTokenId.EVM.USDT toToken === KnownTokenId.EVM.USDT
) { ) {
return stxAlternativeTokenContractAddresses.usdt[route.fromChain] return terminatingStacksTokenContractAddresses.usdt[route.fromChain]
} }
assertExclude(restChains, KnownChainId.EVM.Ethereum) assertExclude(restChains, KnownChainId.EVM.Ethereum)
assertExclude(restChains, KnownChainId.EVM.Sepolia) assertExclude(restChains, KnownChainId.EVM.Sepolia)
@@ -140,7 +371,7 @@ export const getTerminatingStacksTokenContractAddress = (
toChain === KnownChainId.EVM.Base && toChain === KnownChainId.EVM.Base &&
toToken === KnownTokenId.EVM.USDC toToken === KnownTokenId.EVM.USDC
) { ) {
return stxAlternativeTokenContractAddresses.usdt[route.fromChain] return terminatingStacksTokenContractAddresses.usdc[route.fromChain]
} }
assertExclude(restChains, KnownChainId.EVM.Base) assertExclude(restChains, KnownChainId.EVM.Base)
@@ -161,7 +392,7 @@ export const getEVMTokenIdFromTerminatingStacksTokenContractAddress = (route: {
route.evmChain === KnownChainId.EVM.Sepolia) && route.evmChain === KnownChainId.EVM.Sepolia) &&
isStacksContractAddressEqual( isStacksContractAddressEqual(
route.stacksTokenAddress, route.stacksTokenAddress,
stxAlternativeTokenContractAddresses.wbtc[route.stacksChain], terminatingStacksTokenContractAddresses.wbtc[route.stacksChain],
) )
) { ) {
return KnownTokenId.EVM.WBTC return KnownTokenId.EVM.WBTC
@@ -174,7 +405,7 @@ export const getEVMTokenIdFromTerminatingStacksTokenContractAddress = (route: {
route.evmChain === KnownChainId.EVM.BSCTestnet) && route.evmChain === KnownChainId.EVM.BSCTestnet) &&
isStacksContractAddressEqual( isStacksContractAddressEqual(
route.stacksTokenAddress, route.stacksTokenAddress,
stxAlternativeTokenContractAddresses.btcb[route.stacksChain], terminatingStacksTokenContractAddresses.btcb[route.stacksChain],
) )
) { ) {
return KnownTokenId.EVM.BTCB return KnownTokenId.EVM.BTCB
@@ -186,7 +417,7 @@ export const getEVMTokenIdFromTerminatingStacksTokenContractAddress = (route: {
route.evmChain === KnownChainId.EVM.Base && route.evmChain === KnownChainId.EVM.Base &&
isStacksContractAddressEqual( isStacksContractAddressEqual(
route.stacksTokenAddress, route.stacksTokenAddress,
stxAlternativeTokenContractAddresses.cbBTC[route.stacksChain], terminatingStacksTokenContractAddresses.cbBTC[route.stacksChain],
) )
) { ) {
return KnownTokenId.EVM.cbBTC return KnownTokenId.EVM.cbBTC
@@ -200,7 +431,7 @@ export const getEVMTokenIdFromTerminatingStacksTokenContractAddress = (route: {
route.evmChain === KnownChainId.EVM.BSCTestnet) && route.evmChain === KnownChainId.EVM.BSCTestnet) &&
isStacksContractAddressEqual( isStacksContractAddressEqual(
route.stacksTokenAddress, route.stacksTokenAddress,
stxAlternativeTokenContractAddresses.usdt[route.stacksChain], terminatingStacksTokenContractAddresses.usdt[route.stacksChain],
) )
) { ) {
return KnownTokenId.EVM.USDT return KnownTokenId.EVM.USDT
@@ -214,7 +445,7 @@ export const getEVMTokenIdFromTerminatingStacksTokenContractAddress = (route: {
route.evmChain === KnownChainId.EVM.Base && route.evmChain === KnownChainId.EVM.Base &&
isStacksContractAddressEqual( isStacksContractAddressEqual(
route.stacksTokenAddress, route.stacksTokenAddress,
stxAlternativeTokenContractAddresses.usdc[route.stacksChain], terminatingStacksTokenContractAddresses.usdc[route.stacksChain],
) )
) { ) {
return KnownTokenId.EVM.USDC return KnownTokenId.EVM.USDC
@@ -237,163 +468,3 @@ type ChainsHaveAlternativeBTC =
| typeof KnownChainId.EVM.BSC | typeof KnownChainId.EVM.BSC
| typeof KnownChainId.EVM.BSCTestnet | typeof KnownChainId.EVM.BSCTestnet
| typeof KnownChainId.EVM.Base | 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<KnownChainId.StacksChain, { deployerAddress: string }>
>
export const stxTokenContractAddresses: Record<
KnownTokenId.StacksToken,
Record<KnownChainId.StacksChain, StacksContractAddress>
> = {
[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",
},
},
}

View File

@@ -1,10 +1,9 @@
import { callReadOnlyFunction } from "@stacks/transactions" import { Response } from "clarity-codegen"
import { CallReadOnlyFunctionFn, Response } from "clarity-codegen"
import { hasLength } from "../utils/arrayHelpers" import { hasLength } from "../utils/arrayHelpers"
import { checkNever } from "../utils/typeHelpers" import { SwapRoute } from "../utils/SwapRouteHelpers"
import { KnownChainId } from "../utils/types/knownIds" import { KnownChainId } from "../utils/types/knownIds"
import { StacksContractAddress } from "../xlinkSdkUtils/types" import { StacksContractAddress } from "../xlinkSdkUtils/types"
import { BridgeSwapRoute_FromBitcoin } from "./createBridgeOrder" import { StacksContractName } from "./stxContractAddresses"
import { import {
executeReadonlyCallXLINK, executeReadonlyCallXLINK,
getStacksContractCallInfo, getStacksContractCallInfo,
@@ -15,35 +14,33 @@ export async function validateBridgeOrder(info: {
commitTx: Uint8Array commitTx: Uint8Array
revealTx: Uint8Array revealTx: Uint8Array
terminatingStacksToken: StacksContractAddress terminatingStacksToken: StacksContractAddress
swapRoute: BridgeSwapRoute_FromBitcoin swapRoute?: SwapRoute
}): Promise<void> { }): Promise<void> {
const contractCallInfo = getStacksContractCallInfo( const contractBaseCallInfo = getStacksContractCallInfo(
info.chainId === KnownChainId.Bitcoin.Mainnet info.chainId === KnownChainId.Bitcoin.Mainnet
? KnownChainId.Stacks.Mainnet ? KnownChainId.Stacks.Mainnet
: KnownChainId.Stacks.Testnet, : 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( throw new Error(
"[validateBridgeOrder_BitcoinToEVM] stacks contract information not found", "[validateBridgeOrder_BitcoinToEVM] stacks contract information not found",
) )
} }
const { commitTx, revealTx, swapRoute } = info const { commitTx, revealTx, swapRoute } = info
const executeOptions = {
deployerAddress: contractCallInfo.deployerAddress,
callReadOnlyFunction: (callOptions =>
callReadOnlyFunction({
...callOptions,
network: contractCallInfo.network,
})) satisfies CallReadOnlyFunctionFn,
}
let resp: Response<any> let resp: Response<any>
if (hasLength(swapRoute, 0)) { if (swapRoute == null || hasLength(swapRoute.swapPools, 0)) {
resp = await executeReadonlyCallXLINK( resp = await executeReadonlyCallXLINK(
contractCallInfo.contractName, contractBaseCallInfo.contractName,
"validate-tx-cross", "validate-tx-cross",
{ {
"commit-tx": { "commit-tx": {
@@ -56,10 +53,33 @@ export async function validateBridgeOrder(info: {
}, },
"token-out-trait": `${info.terminatingStacksToken.deployerAddress}.${info.terminatingStacksToken.contractName}`, "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 { } else {
checkNever(swapRoute)
throw new Error( throw new Error(
`[validateBridgeOrder_BitcoinToEVM] unsupported swap route length: ${(swapRoute as any).length}`, `[validateBridgeOrder_BitcoinToEVM] unsupported swap route length: ${(swapRoute as any).length}`,
) )

View File

@@ -1,32 +1,27 @@
import { NETWORK, TEST_NETWORK } from "@scure/btc-signer"
import { StacksNetwork } from "@stacks/network" import { StacksNetwork } from "@stacks/network"
import { callReadOnlyFunction } from "@stacks/transactions"
import { import {
c32address, CallReadOnlyFunctionFn,
c32addressDecode,
versions as c32addressVersions,
} from "c32check"
import {
composeTxOptionsFactory, composeTxOptionsFactory,
executeReadonlyCallFactory, executeReadonlyCallFactory,
} from "clarity-codegen" } from "clarity-codegen"
import { xlinkContracts } from "../../generated/smartContract/contracts_xlink" import { xlinkContracts } from "../../generated/smartContract/contracts_xlink"
import {
addressToScriptPubKey,
scriptPubKeyToAddress,
} from "../bitcoinUtils/bitcoinHelpers"
import { STACKS_MAINNET, STACKS_TESTNET } from "../config" import { STACKS_MAINNET, STACKS_TESTNET } from "../config"
import { requestAPI } from "../utils/apiHelpers"
import { BigNumber, BigNumberSource } from "../utils/BigNumber" import { BigNumber, BigNumberSource } from "../utils/BigNumber"
import { StacksAddressVersionNotSupportedError } from "../utils/errors"
import { import {
decodeHex, createStacksToken,
encodeHex, KnownChainId,
encodeZeroPrefixedHex, KnownTokenId,
} from "../utils/hexHelpers" } from "../utils/types/knownIds"
import { checkNever } from "../utils/typeHelpers"
import { KnownChainId, KnownTokenId } from "../utils/types/knownIds"
import { StacksContractAddress } from "../xlinkSdkUtils/types"
import { import {
stxContractDeployers, isStacksContractAddressEqual,
StacksContractAddress,
} from "../xlinkSdkUtils/types"
import { SDKGlobalContext } from "../xlinkSdkUtils/types.internal"
import {
StacksContractName,
stxContractAddresses,
stxTokenContractAddresses, stxTokenContractAddresses,
} from "./stxContractAddresses" } 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 composeTxXLINK = composeTxOptionsFactory(xlinkContracts, {})
export const executeReadonlyCallXLINK = executeReadonlyCallFactory( export const executeReadonlyCallXLINK = executeReadonlyCallFactory(
@@ -57,9 +62,7 @@ export const executeReadonlyCallXLINK = executeReadonlyCallFactory(
{}, {},
) )
export const getStacksContractCallInfo = < export const getStacksContractCallInfo = <C extends StacksContractName>(
C extends keyof typeof stxContractDeployers,
>(
chainId: KnownChainId.StacksChain, chainId: KnownChainId.StacksChain,
contractName: C, contractName: C,
): ):
@@ -67,39 +70,72 @@ export const getStacksContractCallInfo = <
| (Omit<StacksContractAddress, "contractName"> & { | (Omit<StacksContractAddress, "contractName"> & {
contractName: C contractName: C
network: StacksNetwork network: StacksNetwork
executeOptions: {
deployerAddress?: string
senderAddress?: string
callReadOnlyFunction?: CallReadOnlyFunctionFn
}
}) => { }) => {
const network = const network =
chainId === KnownChainId.Stacks.Mainnet ? STACKS_MAINNET : STACKS_TESTNET chainId === KnownChainId.Stacks.Mainnet ? STACKS_MAINNET : STACKS_TESTNET
if (stxContractDeployers[contractName][chainId] == null) { if (stxContractAddresses[contractName][chainId] == null) {
return undefined return undefined
} }
return { return {
...stxContractDeployers[contractName][chainId], ...stxContractAddresses[contractName][chainId],
contractName, contractName,
network, 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, chainId: KnownChainId.StacksChain,
tokenId: KnownTokenId.StacksToken, tokenId: KnownTokenId.StacksToken,
): undefined | (StacksContractAddress & { network: StacksNetwork }) => { ): Promise<
if (stxTokenContractAddresses[tokenId]?.[chainId] == null) { 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 return undefined
} }
const network = const network =
chainId === KnownChainId.Stacks.Mainnet ? STACKS_MAINNET : STACKS_TESTNET chainId === KnownChainId.Stacks.Mainnet ? STACKS_MAINNET : STACKS_TESTNET
return { return { ...address, network }
...stxTokenContractAddresses[tokenId]![chainId],
network,
}
} }
export async function getStacksToken( export async function getStacksToken(
ctx: SDKGlobalContext,
chain: KnownChainId.StacksChain, chain: KnownChainId.StacksChain,
tokenAddress: StacksContractAddress, tokenAddress: StacksContractAddress,
): Promise<undefined | KnownTokenId.StacksToken> { ): Promise<undefined | KnownTokenId.StacksToken> {
@@ -109,90 +145,87 @@ export async function getStacksToken(
const info = stxTokenContractAddresses[token]?.[chain] const info = stxTokenContractAddresses[token]?.[chain]
if (info == null) continue if (info == null) continue
if ( if (isStacksContractAddressEqual(info, tokenAddress)) {
info.deployerAddress === tokenAddress.deployerAddress && return token as KnownTokenId.StacksToken
info.contractName === tokenAddress.contractName }
) { }
return token
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 return
} }
export function addressFromBuffer( const getAllStacksTokens = (
chain: KnownChainId.KnownChain, ctx: SDKGlobalContext,
buffer: Uint8Array, chain: KnownChainId.StacksChain,
): string { ): Promise<StacksTokenInfo[]> => {
if (KnownChainId.isStacksChain(chain)) { const cache = ctx.stacks.tokensCache
return c32address(
c32addressVersions[ if (cache == null) {
chain === KnownChainId.Stacks.Mainnet ? "mainnet" : "testnet" return getAllStacksTokensImpl(ctx, chain)
].p2pkh,
encodeHex(buffer),
)
} }
if ( const cached = cache.get(chain)
KnownChainId.isBitcoinChain(chain) || if (cached == null) {
KnownChainId.isBRC20Chain(chain) || const promise = getAllStacksTokensImpl(ctx, chain).catch(e => {
KnownChainId.isRunesChain(chain) if (cache.get(chain) === promise) {
) { cache.delete(chain)
return scriptPubKeyToAddress( }
chain === KnownChainId.Bitcoin.Mainnet ? NETWORK : TEST_NETWORK, throw e
buffer, })
) cache.set(chain, promise)
} }
return cache.get(chain)!
if (KnownChainId.isEVMChain(chain)) {
return encodeZeroPrefixedHex(buffer)
}
checkNever(chain)
throw new TypeError("[addressFromBuffer] Unsupported chain: " + chain)
} }
const getAllStacksTokensImpl = async (
export function addressToBuffer( ctx: SDKGlobalContext,
chain: KnownChainId.KnownChain, chain: KnownChainId.StacksChain,
address: string, ): Promise<StacksTokenInfo[]> => {
): Uint8Array { const res = await requestAPI<{ tokens: StacksTokenFromAPI[] }>(ctx, {
if (KnownChainId.isStacksChain(chain)) { method: "GET",
const [version, hash160] = c32addressDecode(address) path: "/2024-10-01/stacks/tokens",
query: {
if ( network: chain === KnownChainId.Stacks.Mainnet ? "mainnet" : "testnet",
(chain === KnownChainId.Stacks.Mainnet && },
version == c32addressVersions.mainnet.p2sh) || })
(chain === KnownChainId.Stacks.Testnet && return res.tokens.map(info => ({
version == c32addressVersions.testnet.p2sh) stacksTokenId: createStacksToken(info.id),
) { contractAddress: info.contractAddress,
throw new StacksAddressVersionNotSupportedError(address, "Multisig") decimals: info.decimals,
} else if ( underlyingToken: info.underlyingToken,
(chain === KnownChainId.Stacks.Mainnet && }))
version !== c32addressVersions.mainnet.p2pkh) || }
(chain === KnownChainId.Stacks.Testnet && export interface StacksTokenInfo {
version !== c32addressVersions.testnet.p2pkh) stacksTokenId: KnownTokenId.StacksToken
) { contractAddress: StacksContractAddress
throw new StacksAddressVersionNotSupportedError(address, `${version}`) decimals: number
} underlyingToken?: {
contractAddress: StacksContractAddress
return decodeHex(hash160) decimals: number
} }
}
if ( interface StacksTokenFromAPI {
KnownChainId.isBitcoinChain(chain) || id: string
KnownChainId.isBRC20Chain(chain) || contractAddress: StacksContractAddress
KnownChainId.isRunesChain(chain) decimals: number
) { underlyingToken?: {
return addressToScriptPubKey( contractAddress: StacksContractAddress
chain === KnownChainId.Bitcoin.Mainnet ? NETWORK : TEST_NETWORK, decimals: number
address, }
)
}
if (KnownChainId.isEVMChain(chain)) {
return decodeHex(address)
}
checkNever(chain)
throw new TypeError("[addressToBuffer] Unsupported chain: " + chain)
} }

110
src/utils/Optional.ts Normal file
View File

@@ -0,0 +1,110 @@
export type Optional<T> = Optional.Some<T> | Optional.None
export namespace Optional {
export interface Some<T> {
type: "some"
payload: T
}
export interface None {
type: "none"
}
export const isOptional = <T>(input: any): input is Optional<T> => {
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 = <const TOK>(payload: TOK): Optional.Some<TOK> => ({
type: "some",
payload,
})
export const none = (): Optional.None => ({
type: "none",
})
export const maybeSome = <T>(res: Optional<T>): undefined | T => {
if (res.type === "some") return res.payload
return
}
export function encaseOptional<TOK>(fn: () => TOK): Optional<TOK> {
try {
return some(fn())
} catch (e: unknown) {
return none()
}
}
export function encaseMaybeSome<TOK>(fn: () => TOK): undefined | TOK
export function encaseMaybeSome<TOK, TFallback>(
fallbackValue: TFallback,
fn: () => TOK,
): TOK | TFallback
export function encaseMaybeSome<TOK, TFallback>(
...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 {
<TOKInput, TOKOutput>(
mapping: (payload: TOKInput) => TOKOutput,
): (res: Optional<TOKInput>) => Optional<TOKOutput>
<TOKInput, TOKOutput>(
mapping: (payload: TOKInput) => TOKOutput,
res: Optional<TOKInput>,
): Optional<TOKOutput>
}
export const map: CurriedMapFn = <TOKInput, TOKOutput>(
mapping: (payload: TOKInput) => TOKOutput,
res?: Optional<TOKInput>,
): any => {
if (res == null) {
return (res: Optional<TOKInput>) => realMap(mapping, res)
} else {
return realMap(mapping, res)
}
function realMap(
mapping: (payload: TOKInput) => TOKOutput,
res: Optional<TOKInput>,
): Optional<TOKOutput> {
if (res.type === "none") return res
return Optional.some(mapping(res.payload))
}
}
interface CurriedFlatMapFn {
<TOKInput, TOKOutput>(
mapping: (payload: TOKInput) => Optional<TOKOutput>,
): (res: Optional<TOKInput>) => Optional<TOKOutput>
<TOKInput, TOKOutput>(
mapping: (payload: TOKInput) => Optional<TOKOutput>,
res: Optional<TOKInput>,
): Optional<TOKOutput>
}
export const flatMap: CurriedFlatMapFn = <TOKInput, TOKOutput>(
mapping: (payload: TOKInput) => Optional<TOKOutput>,
res?: Optional<TOKInput>,
): any => {
if (res == null) {
return (res: Optional<TOKInput>) => realFlatMap(mapping, res)
} else {
return realFlatMap(mapping, res)
}
function realFlatMap(
mapping: (payload: TOKInput) => Optional<TOKOutput>,
res: Optional<TOKInput>,
): Optional<TOKOutput> {
if (res.type === "none") return res
return mapping(res.payload)
}
}
}

164
src/utils/Result.ts Normal file
View File

@@ -0,0 +1,164 @@
export type Result<TOK, TError> = Result.OK<TOK> | Result.Error<TError>
export namespace Result {
export interface OK<T> {
type: "ok"
payload: T
}
export interface Error<T> {
type: "error"
payload: T
}
export const isResult = <TOK, TErr>(
input: any,
): input is Result<TOK, TErr> => {
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 = <const TOK>(payload: TOK): Result.OK<TOK> => ({
type: "ok",
payload,
})
export const error = <const TError>(
payload: TError,
): Result.Error<TError> => ({
type: "error",
payload,
})
export const maybeValue = <TOK, TError>(
res: Result<TOK, TError>,
): undefined | TOK => {
if (res.type === "ok") return res.payload
return
}
export const maybeError = <TOK, TError>(
res: Result<TOK, TError>,
): undefined | TError => {
if (res.type === "error") return res.payload
return
}
export const encase = <TOK, TError = unknown>(
fn: () => TOK,
): Result<TOK, TError> => {
try {
return ok(fn())
} catch (e: unknown) {
return error(e) as any
}
}
export function encaseMaybeValue<TOK>(fn: () => TOK): undefined | TOK
export function encaseMaybeValue<TOK, TFallback>(
fallbackValue: TFallback,
fn: () => TOK,
): TOK | TFallback
export function encaseMaybeValue<TOK, TFallback>(
...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 {
<TOKInput, TOKOutput, TError>(
mapping: (payload: TOKInput) => TOKOutput,
): (res: Result<TOKInput, TError>) => Result<TOKOutput, TError>
<TOKInput, TOKOutput, TError>(
mapping: (payload: TOKInput) => TOKOutput,
res: Result<TOKInput, TError>,
): Result<TOKOutput, TError>
}
export const map: CurriedMapFn = <TOKInput, TOKOutput, TError>(
mapping: (payload: TOKInput) => TOKOutput,
res?: Result<TOKInput, TError>,
): any => {
if (res == null) {
return (res: Result<TOKInput, TError>) => realMap(mapping, res)
} else {
return realMap(mapping, res)
}
function realMap(
mapping: (payload: TOKInput) => TOKOutput,
res: Result<TOKInput, TError>,
): Result<TOKOutput, TError> {
if (res.type === "error") return res
return Result.ok(mapping(res.payload))
}
}
interface CurriedChainErrorFn {
<TOKInput, TErrorInput, TOKOutput, TErrorOutput>(
mapping: (payload: TErrorInput) => Result<TOKOutput, TErrorOutput>,
): (
res: Result<TOKInput, TErrorInput>,
) => Result<TOKInput | TOKOutput, TErrorOutput>
<TOKInput, TErrorInput, TOKOutput, TErrorOutput>(
mapping: (payload: TErrorInput) => Result<TOKOutput, TErrorOutput>,
res: Result<TOKInput, TErrorInput>,
): Result<TOKInput | TOKOutput, TErrorOutput>
}
export const chainError: CurriedChainErrorFn = <
TOKInput,
TErrorInput,
TOKOutput,
TErrorOutput,
>(
chainFn: (payload: TErrorInput) => Result<TOKOutput, TErrorOutput>,
res?: Result<TOKInput, TErrorInput>,
): any => {
if (res == null) {
return (res: Result<TOKInput, TErrorInput>) =>
realChainError(chainFn, res)
} else {
return realChainError(chainFn, res)
}
function realChainError(
chainFn: (payload: TErrorInput) => Result<TOKOutput, TErrorOutput>,
res: Result<TOKInput, TErrorInput>,
): Result<TOKInput | TOKOutput, TErrorOutput> {
if (res.type !== "error") return res
return chainFn(res.payload)
}
}
interface CurriedFlatMapFn {
<TOKInput, TOKOutput, TError>(
mapping: (payload: TOKInput) => Result<TOKOutput, TError>,
): (res: Result<TOKInput, TError>) => Result<TOKOutput, TError>
<TOKInput, TOKOutput, TError>(
mapping: (payload: TOKInput) => Result<TOKOutput, TError>,
res: Result<TOKInput, TError>,
): Result<TOKOutput, TError>
}
export const flatMap: CurriedFlatMapFn = <TOKInput, TOKOutput, TError>(
mapping: (payload: TOKInput) => Result<TOKOutput, TError>,
res?: Result<TOKInput, TError>,
): any => {
if (res == null) {
return (res: Result<TOKInput, TError>) => realMap(mapping, res)
} else {
return realMap(mapping, res)
}
function realMap(
mapping: (payload: TOKInput) => Result<TOKOutput, TError>,
res: Result<TOKInput, TError>,
): Result<TOKOutput, TError> {
if (res.type === "error") return res
return mapping(res.payload)
}
}
export const chainOk = flatMap
}

View File

@@ -0,0 +1,16 @@
import { identity } from "./funcHelpers"
import { Result } from "./Result"
export function encase<T, T1>(
mapping: (res: T) => T1,
fn: () => T,
): undefined | T1
export function encase<T>(fn: () => T): undefined | T
export function encase<T, T1>(
...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)),
)
}

View File

@@ -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<undefined | KnownTokenId.StacksToken> {
return getStacksToken(
sdkContext,
info.stacksChain,
info.swap.fromTokenAddress,
)
}
export async function getFinalStepStacksTokenAddress(
sdkContext: SDKGlobalContext,
info: {
swap: SwapRoute
stacksChain: KnownChainId.StacksChain
},
): Promise<undefined | KnownTokenId.StacksToken> {
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<undefined | KnownTokenId.StacksToken> {
let toStacksTokenPromise:
| undefined
| Promise<undefined | KnownTokenId.StacksToken>
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
}
}

105
src/utils/addressHelpers.ts Normal file
View File

@@ -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)
}

View File

@@ -1,4 +1,5 @@
import { ChainId, TokenId } from "../xlinkSdkUtils/types" import { ChainId, TokenId } from "../xlinkSdkUtils/types"
import { SwapRoute } from "./SwapRouteHelpers"
import { SDKGlobalContext } from "../xlinkSdkUtils/types.internal" import { SDKGlobalContext } from "../xlinkSdkUtils/types.internal"
import { UnsupportedBridgeRouteError } from "./errors" import { UnsupportedBridgeRouteError } from "./errors"
import { pMemoize } from "./pMemoize" import { pMemoize } from "./pMemoize"
@@ -53,23 +54,23 @@ export type KnownRoute_FromBitcoin_ToEVM = {
toChain: KnownChainId.EVMChain toChain: KnownChainId.EVMChain
toToken: KnownTokenId.EVMToken toToken: KnownTokenId.EVMToken
} }
// export type KnownRoute_FromBitcoin_ToBRC20 = { export type KnownRoute_FromBitcoin_ToBRC20 = {
// fromChain: KnownChainId.BitcoinChain fromChain: KnownChainId.BitcoinChain
// fromToken: KnownTokenId.BitcoinToken fromToken: KnownTokenId.BitcoinToken
// toChain: KnownChainId.BRC20Chain toChain: KnownChainId.BRC20Chain
// toToken: KnownTokenId.BRC20Token toToken: KnownTokenId.BRC20Token
// } }
// export type KnownRoute_FromBitcoin_ToRunes = { export type KnownRoute_FromBitcoin_ToRunes = {
// fromChain: KnownChainId.BitcoinChain fromChain: KnownChainId.BitcoinChain
// fromToken: KnownTokenId.BitcoinToken fromToken: KnownTokenId.BitcoinToken
// toChain: KnownChainId.RunesChain toChain: KnownChainId.RunesChain
// toToken: KnownTokenId.RunesToken toToken: KnownTokenId.RunesToken
// } }
export type KnownRoute_FromBitcoin = export type KnownRoute_FromBitcoin =
| KnownRoute_FromBitcoin_ToStacks | KnownRoute_FromBitcoin_ToStacks
| KnownRoute_FromBitcoin_ToEVM | KnownRoute_FromBitcoin_ToEVM
// | KnownRoute_FromBitcoin_ToBRC20 | KnownRoute_FromBitcoin_ToBRC20
// | KnownRoute_FromBitcoin_ToRunes | KnownRoute_FromBitcoin_ToRunes
export type KnownRoute_FromEVM_ToStacks = { export type KnownRoute_FromEVM_ToStacks = {
fromChain: KnownChainId.EVMChain fromChain: KnownChainId.EVMChain
@@ -108,24 +109,98 @@ export type KnownRoute_FromEVM =
| KnownRoute_FromEVM_ToRunes | KnownRoute_FromEVM_ToRunes
| KnownRoute_FromEVM_ToEVM | KnownRoute_FromEVM_ToEVM
export type _KnownRoute_FromBRC20_ToStacks = { export type KnownRoute_FromBRC20_ToStacks = {
fromChain: KnownChainId.BRC20Chain fromChain: KnownChainId.BRC20Chain
fromToken: KnownTokenId.BRC20Token fromToken: KnownTokenId.BRC20Token
toChain: KnownChainId.StacksChain toChain: KnownChainId.StacksChain
toToken: KnownTokenId.StacksToken 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 fromChain: KnownChainId.RunesChain
fromToken: KnownTokenId.RunesToken fromToken: KnownTokenId.RunesToken
toChain: KnownChainId.StacksChain toChain: KnownChainId.StacksChain
toToken: KnownTokenId.StacksToken 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 = export type KnownRoute =
| KnownRoute_FromStacks | KnownRoute_FromStacks
| KnownRoute_FromBitcoin | KnownRoute_FromBitcoin
| KnownRoute_FromEVM | 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( export function defineRoute(
chainPairs: [fromChains: ChainId[], toChains: ChainId[]], chainPairs: [fromChains: ChainId[], toChains: ChainId[]],
tokenPairs: [fromToken: TokenId, toToken: TokenId][], tokenPairs: [fromToken: TokenId, toToken: TokenId][],
@@ -150,7 +225,9 @@ export function defineRoute(
export type IsSupportedFn = ( export type IsSupportedFn = (
ctx: SDKGlobalContext, ctx: SDKGlobalContext,
route: DefinedRoute, route: DefinedRoute & {
swapRoute?: SwapRoute
},
) => Promise<boolean> ) => Promise<boolean>
const memoizedIsSupportedFactory = ( const memoizedIsSupportedFactory = (
isSupported: IsSupportedFn, isSupported: IsSupportedFn,
@@ -158,7 +235,12 @@ const memoizedIsSupportedFactory = (
return pMemoize( return pMemoize(
{ {
cacheKey([, route]) { 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, skipCache: true,
}, },
@@ -168,8 +250,8 @@ const memoizedIsSupportedFactory = (
export interface GetSupportedRoutesFn_Conditions { export interface GetSupportedRoutesFn_Conditions {
fromChain?: ChainId fromChain?: ChainId
toChain?: ChainId
fromToken?: TokenId fromToken?: TokenId
toChain?: ChainId
toToken?: TokenId toToken?: TokenId
} }
export type GetSupportedRoutesFn = ( export type GetSupportedRoutesFn = (
@@ -179,7 +261,9 @@ export type GetSupportedRoutesFn = (
export type CheckRouteValidFn = ( export type CheckRouteValidFn = (
ctx: SDKGlobalContext, ctx: SDKGlobalContext,
route: DefinedRoute, route: DefinedRoute & {
swapRoute?: SwapRoute
},
) => Promise<KnownRoute> ) => Promise<KnownRoute>
export function buildSupportedRoutes( export function buildSupportedRoutes(

View File

@@ -19,12 +19,34 @@ export interface TransferProphetAppliedResult {
export const applyTransferProphets = ( export const applyTransferProphets = (
transferProphets: OneOrMore<TransferProphet>, transferProphets: OneOrMore<TransferProphet>,
amount: BigNumber, amount: BigNumber,
options: {
exchangeRates?: readonly BigNumber[]
} = {},
): OneOrMore<TransferProphetAppliedResult> => { ): OneOrMore<TransferProphetAppliedResult> => {
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( return reduce(
(acc, transferProphet) => (acc, transferProphet, idx) =>
concat(acc, [applyTransferProphet(transferProphet, last(acc).netAmount)]), concat(acc, [
[{ fees: [], netAmount: amount }], applyTransferProphet(
transferProphets, 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 * @example
* composeTransferProphets( * composeTransferProphets(
* [ * [
@@ -92,6 +117,15 @@ export const composeTransferProphets = (
transferProphets: readonly TransferProphet[], transferProphets: readonly TransferProphet[],
exchangeRates: readonly BigNumber[], exchangeRates: readonly BigNumber[],
): TransferProphetAggregated<TransferProphet[]> => { ): TransferProphetAggregated<TransferProphet[]> => {
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) const cumulativeExchangeRates = calcCumulativeExchangeRates(exchangeRates)
return reduce( return reduce(
@@ -99,7 +133,7 @@ export const composeTransferProphets = (
...composeTransferProphet2( ...composeTransferProphet2(
res, res,
transferProphet, transferProphet,
cumulativeExchangeRates[idx], cumulativeExchangeRates[idx + 1],
), ),
transferProphets: [...res.transferProphets, transferProphet], transferProphets: [...res.transferProphets, transferProphet],
}), }),
@@ -114,18 +148,18 @@ export const composeTransferProphets = (
/** /**
* @example * @example
* calcCumulativeExchangeRates([ * calcCumulativeExchangeRates([
* exchangeRateOf(tokenA, tokenB), * exchangeRateOf(A, B),
* exchangeRateOf(tokenB, tokenC), * exchangeRateOf(B, C),
* exchangeRateOf(tokenC, tokenD), * exchangeRateOf(C, D),
* // ... * // ...
* ]) * ])
* *
* // => * // =>
* *
* [ * [
* exchangeRateOf(tokenA, tokenB), * exchangeRateOf(A, B),
* exchangeRateOf(tokenA, tokenC), * exchangeRateOf(A, C),
* exchangeRateOf(tokenA, tokenD), * exchangeRateOf(A, D),
* // ... * // ...
* ] * ]
*/ */
@@ -140,30 +174,6 @@ export const composeTransferProphet2 = (
transferProphet2: TransferProphet, transferProphet2: TransferProphet,
exchangeRate: BigNumber, exchangeRate: BigNumber,
): TransferProphetAggregated<[TransferProphet, TransferProphet]> => { ): 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 - (amount1 * (1-feeRate1) * (1-feeRate2) * (1-feeRateN...) / amount1)
* flatFeeRate = 1 - (1-feeRate1) * (1-feeRate2) * (1-feeRateN...) * 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-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( const step1ToStep2Rate = BigNumber.mul(
BigNumber.minus(1, step1FlatFeeRate), BigNumber.minus(1, step1FlatFeeRate),
exchangeRate, 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 let minBridgeAmount: BigNumber | null = null
if ( if (
BigNumber.isZero(bridgeTokenMinFeeAmount) /* min fee amount not set */ && BigNumber.isZero(bridgeTokenMinFeeAmount) /* min fee amount not set */ &&

1
src/utils/funcHelpers.ts Normal file
View File

@@ -0,0 +1 @@
export const identity = <T>(x: T): T => x

View File

@@ -0,0 +1,6 @@
export function entries<T>(obj: T): [keyof T, T[keyof T]][] {
return Object.entries(obj as any) as [keyof T, T[keyof T]][]
}
export function fromEntries<T>(entries: [keyof T, T[keyof T]][]): T {
return Object.fromEntries(entries) as T
}

View File

@@ -4,12 +4,12 @@ import {
toSDKNumberOrUndefined, toSDKNumberOrUndefined,
} from "../../xlinkSdkUtils/types" } from "../../xlinkSdkUtils/types"
import { BigNumber } from "../BigNumber" import { BigNumber } from "../BigNumber"
import { last } from "../arrayHelpers" import { first, last } from "../arrayHelpers"
import { KnownRoute } from "../buildSupportedRoutes" import { KnownRoute_WithMetaProtocol } from "../buildSupportedRoutes"
import { import {
applyTransferProphet, applyTransferProphet,
applyTransferProphets, applyTransferProphets,
composeTransferProphets, composeTransferProphet2,
} from "../feeRateHelpers" } from "../feeRateHelpers"
import { checkNever, isNotNull, OneOrMore } from "../typeHelpers" import { checkNever, isNotNull, OneOrMore } from "../typeHelpers"
import { KnownChainId, KnownTokenId } from "./knownIds" import { KnownChainId, KnownTokenId } from "./knownIds"
@@ -98,7 +98,7 @@ export function transformFromPublicTransferProphet(
} }
} }
export function transformToPublicTransferProphet( export function transformToPublicTransferProphet(
route: KnownRoute, route: KnownRoute_WithMetaProtocol,
fromAmount: SDKNumber | BigNumber, fromAmount: SDKNumber | BigNumber,
transferProphet: TransferProphet, transferProphet: TransferProphet,
): PublicTransferProphet { ): PublicTransferProphet {
@@ -155,20 +155,28 @@ export function transformToPublicTransferProphet(
* ], * ],
* ) * )
*/ */
export const transformToPublicTransferProphetAggregated = ( export const transformToPublicTransferProphetAggregated2 = (
transferProphets: OneOrMore<PublicTransferProphet>, routes: [KnownRoute_WithMetaProtocol, KnownRoute_WithMetaProtocol],
exchangeRates: readonly BigNumber[], transferProphets: [TransferProphet, TransferProphet],
): PublicTransferProphetAggregated<OneOrMore<PublicTransferProphet>> => { fromAmount: BigNumber,
const firstTransferProphet = transferProphets[0] exchangeRate: BigNumber,
const lastTransferProphet = last(transferProphets) ): 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( const composed = composeTransferProphet2(
transformFromPublicTransferProphet, transferProphets[0],
) as any as OneOrMore<TransferProphet> transferProphets[1],
const composed = composeTransferProphets(steps, exchangeRates) exchangeRate,
)
const fromAmount = BigNumber.from(firstTransferProphet.fromAmount) const applyResult = applyTransferProphets(transferProphets, fromAmount, {
const applyResult = applyTransferProphets(steps, fromAmount) exchangeRates: [exchangeRate],
})
const fees: PublicTransferProphet["fees"] = applyResult const fees: PublicTransferProphet["fees"] = applyResult
.flatMap(r => r.fees) .flatMap(r => r.fees)
@@ -189,17 +197,30 @@ export const transformToPublicTransferProphetAggregated = (
) )
.filter(isNotNull) .filter(isNotNull)
const firstRoute = first(routes)
const lastRoute = last(routes)
return { return {
fromChain: firstTransferProphet.fromChain, fromChain: firstRoute.fromChain,
fromToken: firstTransferProphet.fromToken, fromToken: firstRoute.fromToken,
toChain: lastTransferProphet.toChain, toChain: lastRoute.toChain,
toToken: lastTransferProphet.toToken, toToken: lastRoute.toToken,
fromAmount: toSDKNumberOrUndefined(fromAmount), fromAmount: toSDKNumberOrUndefined(fromAmount),
toAmount: toSDKNumberOrUndefined(last(applyResult).netAmount), toAmount: toSDKNumberOrUndefined(last(applyResult).netAmount),
isPaused: composed.isPaused, isPaused: composed.isPaused,
fees, fees,
minBridgeAmount: toSDKNumberOrUndefined(composed.minBridgeAmount), minBridgeAmount: toSDKNumberOrUndefined(composed.minBridgeAmount),
maxBridgeAmount: toSDKNumberOrUndefined(composed.maxBridgeAmount), maxBridgeAmount: toSDKNumberOrUndefined(composed.maxBridgeAmount),
transferProphets, transferProphets: [
transformToPublicTransferProphet(
routes[0],
fromAmount,
transferProphets[0],
),
transformToPublicTransferProphet(
routes[1],
applyResult[1].netAmount,
transferProphets[1],
),
],
} }
} }

View File

@@ -104,16 +104,21 @@ export namespace KnownTokenId {
export const vLiSTX = tokenId("stx-vlistx") export const vLiSTX = tokenId("stx-vlistx")
/** Represents the vLiALEX token ID on the Stacks blockchain. */ /** Represents the vLiALEX token ID on the Stacks blockchain. */
export const vLiALEX = tokenId("stx-vlialex") export const vLiALEX = tokenId("stx-vlialex")
export const vLiaBTC = tokenId("stx-vliabtc")
export const uBTC = tokenId("stx-ubtc") export const uBTC = tokenId("stx-ubtc")
export const DB20 = tokenId("stx-db20") export const DB20 = tokenId("stx-db20")
export const DOG = tokenId("stx-dog") export const DOG = tokenId("stx-dog")
} }
/** This type includes all known tokens on the Stacks blockchain. */ export type StacksToken = TokenId<`stx-${string}`>
export type StacksToken = (typeof _allKnownStacksTokens)[number]
export function isStacksToken(value: TokenId): value is StacksToken { 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 = ( export const createBRC20Token = (
brc20tick: string, brc20tick: string,
): KnownTokenId.BRC20Token => { ): KnownTokenId.BRC20Token => {
@@ -126,7 +131,6 @@ export const createRunesToken = (
} }
export const _allKnownBitcoinTokens = Object.values(KnownTokenId.Bitcoin) export const _allKnownBitcoinTokens = Object.values(KnownTokenId.Bitcoin)
export const _allKnownEVMTokens = Object.values(KnownTokenId.EVM) export const _allKnownEVMTokens = Object.values(KnownTokenId.EVM)
export const _allKnownStacksTokens = Object.values(KnownTokenId.Stacks)
/** /**
* The `KnownChainId` namespace provides types of blockchain networks * The `KnownChainId` namespace provides types of blockchain networks

View File

@@ -12,27 +12,27 @@ import {
prepareTransaction, prepareTransaction,
} from "../bitcoinUtils/prepareTransaction" } from "../bitcoinUtils/prepareTransaction"
import { import {
BridgeSwapRoute_FromBitcoin,
CreateBridgeOrderResult, CreateBridgeOrderResult,
createBridgeOrder_BitcoinToEVM, createBridgeOrder_BitcoinToEVM,
createBridgeOrder_BitcoinToMeta,
createBridgeOrder_BitcoinToStacks, createBridgeOrder_BitcoinToStacks,
} from "../stacksUtils/createBridgeOrder" } from "../stacksUtils/createBridgeOrderFromBitcoin"
import { validateBridgeOrder } from "../stacksUtils/validateBridgeOrder" import { validateBridgeOrder } from "../stacksUtils/validateBridgeOrder"
import { import { getStacksTokenContractInfo } from "../stacksUtils/xlinkContractHelpers"
getStacksTokenContractInfo,
numberToStacksContractNumber,
} from "../stacksUtils/xlinkContractHelpers"
import { range } from "../utils/arrayHelpers" import { range } from "../utils/arrayHelpers"
import { BigNumber } from "../utils/BigNumber" import { BigNumber } from "../utils/BigNumber"
import { import {
KnownRoute_FromBitcoin, KnownRoute_FromBitcoin,
KnownRoute_FromBitcoin_ToBRC20,
KnownRoute_FromBitcoin_ToEVM, KnownRoute_FromBitcoin_ToEVM,
KnownRoute_FromBitcoin_ToRunes,
KnownRoute_FromBitcoin_ToStacks, KnownRoute_FromBitcoin_ToStacks,
buildSupportedRoutes, buildSupportedRoutes,
defineRoute, defineRoute,
} from "../utils/buildSupportedRoutes" } from "../utils/buildSupportedRoutes"
import { import {
BridgeValidateFailedError, BridgeValidateFailedError,
InvalidMethodParametersError,
UnsupportedBridgeRouteError, UnsupportedBridgeRouteError,
} from "../utils/errors" } from "../utils/errors"
import { decodeHex } from "../utils/hexHelpers" import { decodeHex } from "../utils/hexHelpers"
@@ -42,8 +42,11 @@ import {
KnownTokenId, KnownTokenId,
_allKnownEVMMainnetChains, _allKnownEVMMainnetChains,
_allKnownEVMTestnetChains, _allKnownEVMTestnetChains,
_knownChainIdToErrorMessagePart,
} from "../utils/types/knownIds" } from "../utils/types/knownIds"
import { ChainId, SDKNumber, TokenId } from "./types" import { ChainId, SDKNumber, TokenId } from "./types"
import { SwapRoute_WithMinimumAmountsToReceive_Public } from "../utils/SwapRouteHelpers"
import { SwapRoute } from "../utils/SwapRouteHelpers"
import { SDKGlobalContext } from "./types.internal" import { SDKGlobalContext } from "./types.internal"
export const supportedRoutes = buildSupportedRoutes( export const supportedRoutes = buildSupportedRoutes(
@@ -98,8 +101,13 @@ export interface BridgeFromBitcoinInput {
fromAddress: string fromAddress: string
fromAddressScriptPubKey: Uint8Array fromAddressScriptPubKey: Uint8Array
toAddress: string toAddress: string
/**
* **Required** when `toChain` is one of bitcoin chains
*/
toAddressScriptPubKey?: Uint8Array
amount: SDKNumber amount: SDKNumber
networkFeeRate: bigint networkFeeRate: bigint
swapRoute?: SwapRoute_WithMinimumAmountsToReceive_Public
reselectSpendableUTXOs: ReselectSpendableUTXOsFn reselectSpendableUTXOs: ReselectSpendableUTXOsFn
signPsbt: BridgeFromBitcoinInput_signPsbtFn signPsbt: BridgeFromBitcoinInput_signPsbtFn
sendTransaction: (tx: { hex: string }) => Promise<{ sendTransaction: (tx: { hex: string }) => Promise<{
@@ -144,13 +152,32 @@ export async function bridgeFromBitcoin(
toToken: route.toToken, toToken: route.toToken,
}) })
} }
} else if ( } else if (KnownChainId.isBRC20Chain(route.toChain)) {
KnownChainId.isBRC20Chain(route.toChain) || if (
KnownChainId.isRunesChain(route.toChain) KnownTokenId.isBitcoinToken(route.fromToken) &&
) { KnownTokenId.isBRC20Token(route.toToken)
assertExclude(route.toChain, assertExclude.i<KnownChainId.BRC20Chain>()) ) {
assertExclude(route.toChain, assertExclude.i<KnownChainId.RunesChain>()) return bridgeFromBitcoin_toMeta(ctx, {
// TODO: bitcoin to brc20/runes is not supported yet ...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 { } else {
assertExclude(route.toChain, assertExclude.i<KnownChainId.BitcoinChain>()) assertExclude(route.toChain, assertExclude.i<KnownChainId.BitcoinChain>())
checkNever(route) checkNever(route)
@@ -170,7 +197,7 @@ export async function bridgeFromBitcoin(
} }
async function bridgeFromBitcoin_toStacks( async function bridgeFromBitcoin_toStacks(
sdkContext: Pick<SDKGlobalContext, "backendAPI">, sdkContext: SDKGlobalContext,
info: Omit< info: Omit<
BridgeFromBitcoinInput, BridgeFromBitcoinInput,
"fromChain" | "toChain" | "fromToken" | "toToken" "fromChain" | "toChain" | "fromToken" | "toToken"
@@ -178,7 +205,8 @@ async function bridgeFromBitcoin_toStacks(
KnownRoute_FromBitcoin_ToStacks, KnownRoute_FromBitcoin_ToStacks,
): Promise<BridgeFromBitcoinOutput> { ): Promise<BridgeFromBitcoinOutput> {
const pegInAddress = getBTCPegInAddress(info.fromChain, info.toChain) const pegInAddress = getBTCPegInAddress(info.fromChain, info.toChain)
const toTokenContractInfo = getStacksTokenContractInfo( const toTokenContractInfo = await getStacksTokenContractInfo(
sdkContext,
info.toChain, info.toChain,
info.toToken, info.toToken,
) )
@@ -190,14 +218,21 @@ async function bridgeFromBitcoin_toStacks(
) )
} }
const createdOrder = await createBridgeOrder_BitcoinToStacks({ const createdOrder = await createBridgeOrder_BitcoinToStacks(sdkContext, {
fromChain: info.fromChain, fromChain: info.fromChain,
fromBitcoinScriptPubKey: info.fromAddressScriptPubKey, fromBitcoinScriptPubKey: info.fromAddressScriptPubKey,
toChain: info.toChain, toChain: info.toChain,
toToken: info.toToken, toToken: info.toToken,
toStacksAddress: info.toAddress, toStacksAddress: info.toAddress,
swapSlippedAmount: numberToStacksContractNumber(info.amount), swap:
swapRoute: [], info.swapRoute == null
? undefined
: {
...info.swapRoute,
minimumAmountsToReceive: BigNumber.from(
info.swapRoute.minimumAmountsToReceive,
),
},
}) })
if (createdOrder == null) { if (createdOrder == null) {
throw new UnsupportedBridgeRouteError( throw new UnsupportedBridgeRouteError(
@@ -211,21 +246,77 @@ async function bridgeFromBitcoin_toStacks(
} }
async function bridgeFromBitcoin_toEVM( async function bridgeFromBitcoin_toEVM(
sdkContext: Pick<SDKGlobalContext, "backendAPI">, sdkContext: SDKGlobalContext,
info: Omit< info: Omit<
BridgeFromBitcoinInput, BridgeFromBitcoinInput,
"fromChain" | "toChain" | "fromToken" | "toToken" "fromChain" | "toChain" | "fromToken" | "toToken"
> & > &
KnownRoute_FromBitcoin_ToEVM, KnownRoute_FromBitcoin_ToEVM,
): Promise<BridgeFromBitcoinOutput> { ): Promise<BridgeFromBitcoinOutput> {
const createdOrder = await createBridgeOrder_BitcoinToEVM({ const createdOrder = await createBridgeOrder_BitcoinToEVM(sdkContext, {
fromChain: info.fromChain, fromChain: info.fromChain,
toChain: info.toChain, toChain: info.toChain,
toToken: info.toToken, toToken: info.toToken,
fromBitcoinScriptPubKey: info.fromAddressScriptPubKey, fromBitcoinScriptPubKey: info.fromAddressScriptPubKey,
toEVMAddress: info.toAddress, toEVMAddress: info.toAddress,
swapSlippedAmount: numberToStacksContractNumber(info.amount), swap:
swapRoute: [], 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<BridgeFromBitcoinOutput> {
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) { if (createdOrder == null) {
throw new UnsupportedBridgeRouteError( throw new UnsupportedBridgeRouteError(
@@ -239,7 +330,7 @@ async function bridgeFromBitcoin_toEVM(
} }
async function broadcastBitcoinTransaction( async function broadcastBitcoinTransaction(
sdkContext: Pick<SDKGlobalContext, "backendAPI">, sdkContext: SDKGlobalContext,
info: Omit< info: Omit<
ConstructBitcoinTransactionInput, ConstructBitcoinTransactionInput,
"validateBridgeOrder" | "orderData" | "pegInAddress" "validateBridgeOrder" | "orderData" | "pegInAddress"
@@ -324,11 +415,11 @@ type ConstructBitcoinTransactionInput = PrepareBitcoinTransactionInput & {
validateBridgeOrder: ( validateBridgeOrder: (
pegInTx: Uint8Array, pegInTx: Uint8Array,
revealTx: undefined | Uint8Array, revealTx: undefined | Uint8Array,
swapRoute: BridgeSwapRoute_FromBitcoin, swapRoute?: SwapRoute,
) => Promise<void> ) => Promise<void>
} }
async function constructBitcoinTransaction( async function constructBitcoinTransaction(
sdkContext: Pick<SDKGlobalContext, "backendAPI">, sdkContext: SDKGlobalContext,
info: ConstructBitcoinTransactionInput, info: ConstructBitcoinTransactionInput,
): Promise<{ ): Promise<{
hex: string hex: string
@@ -375,9 +466,16 @@ async function constructBitcoinTransaction(
} }
await info await info
.validateBridgeOrder(signedTx.extract(), revealTx, []) .validateBridgeOrder(signedTx.extract(), revealTx, info.swapRoute)
.catch(err => { .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 { return {
@@ -391,6 +489,7 @@ export type PrepareBitcoinTransactionInput = KnownRoute_FromBitcoin & {
fromAddress: BridgeFromBitcoinInput["fromAddress"] fromAddress: BridgeFromBitcoinInput["fromAddress"]
toAddress: BridgeFromBitcoinInput["toAddress"] toAddress: BridgeFromBitcoinInput["toAddress"]
amount: BridgeFromBitcoinInput["amount"] amount: BridgeFromBitcoinInput["amount"]
swapRoute?: BridgeFromBitcoinInput["swapRoute"]
networkFeeRate: BridgeFromBitcoinInput["networkFeeRate"] networkFeeRate: BridgeFromBitcoinInput["networkFeeRate"]
reselectSpendableUTXOs: BridgeFromBitcoinInput["reselectSpendableUTXOs"] reselectSpendableUTXOs: BridgeFromBitcoinInput["reselectSpendableUTXOs"]
orderData: Uint8Array orderData: Uint8Array

View File

@@ -11,10 +11,8 @@ import {
numberToSolidityContractNumber, numberToSolidityContractNumber,
} from "../evmUtils/xlinkContractHelpers" } from "../evmUtils/xlinkContractHelpers"
import { contractAssignedChainIdFromKnownChain } from "../stacksUtils/crossContractDataMapping" import { contractAssignedChainIdFromKnownChain } from "../stacksUtils/crossContractDataMapping"
import { import { getStacksTokenContractInfo } from "../stacksUtils/xlinkContractHelpers"
addressToBuffer, import { addressToBuffer } from "../utils/addressHelpers"
getStacksTokenContractInfo,
} from "../stacksUtils/xlinkContractHelpers"
import { BigNumber } from "../utils/BigNumber" import { BigNumber } from "../utils/BigNumber"
import { import {
buildSupportedRoutes, buildSupportedRoutes,
@@ -350,6 +348,7 @@ async function bridgeFromEVM_toStacks(
info.fromToken, info.fromToken,
) )
const toTokenContractInfo = await getStacksTokenContractInfo( const toTokenContractInfo = await getStacksTokenContractInfo(
ctx,
info.toChain, info.toChain,
info.toToken, info.toToken,
) )
@@ -639,7 +638,7 @@ async function bridgeFromEVM_toMeta(
throw new InvalidMethodParametersError( throw new InvalidMethodParametersError(
[ [
"XLinkSDK", "XLinkSDK",
`bridgeFromEVM (to ${KnownChainId.isBRC20Chain(info.toChain) ? "BRC20" : "Runes"})`, `bridgeFromEVM (to ${_knownChainIdToErrorMessagePart(info.toChain)})`,
], ],
[ [
{ {

View File

@@ -3,6 +3,10 @@ import { ContractCallOptions } from "clarity-codegen"
import { addressToScriptPubKey } from "../bitcoinUtils/bitcoinHelpers" import { addressToScriptPubKey } from "../bitcoinUtils/bitcoinHelpers"
import { contractAssignedChainIdFromKnownChain } from "../stacksUtils/crossContractDataMapping" import { contractAssignedChainIdFromKnownChain } from "../stacksUtils/crossContractDataMapping"
import { isSupportedStacksRoute } from "../stacksUtils/peggingHelpers" import { isSupportedStacksRoute } from "../stacksUtils/peggingHelpers"
import {
getTerminatingStacksTokenContractAddress,
StacksContractName,
} from "../stacksUtils/stxContractAddresses"
import { import {
composeTxXLINK, composeTxXLINK,
getStacksContractCallInfo, getStacksContractCallInfo,
@@ -21,14 +25,13 @@ import { UnsupportedBridgeRouteError } from "../utils/errors"
import { decodeHex } from "../utils/hexHelpers" import { decodeHex } from "../utils/hexHelpers"
import { assertExclude, checkNever } from "../utils/typeHelpers" import { assertExclude, checkNever } from "../utils/typeHelpers"
import { import {
KnownChainId,
KnownTokenId,
_allKnownEVMMainnetChains, _allKnownEVMMainnetChains,
_allKnownEVMTestnetChains, _allKnownEVMTestnetChains,
KnownChainId,
KnownTokenId,
} from "../utils/types/knownIds" } from "../utils/types/knownIds"
import { ChainId, SDKNumber, TokenId } from "./types" import { ChainId, SDKNumber, TokenId } from "./types"
import { SDKGlobalContext } from "./types.internal" import { SDKGlobalContext } from "./types.internal"
import { getTerminatingStacksTokenContractAddress } from "../stacksUtils/stxContractAddresses"
export const supportedRoutes = buildSupportedRoutes( export const supportedRoutes = buildSupportedRoutes(
[ [
@@ -139,7 +142,7 @@ export async function bridgeFromStacks(
KnownTokenId.isStacksToken(route.fromToken) && KnownTokenId.isStacksToken(route.fromToken) &&
KnownTokenId.isEVMToken(route.toToken) KnownTokenId.isEVMToken(route.toToken)
) { ) {
return bridgeFromStacks_toEVM({ return bridgeFromStacks_toEVM(ctx, {
...info, ...info,
fromChain: route.fromChain, fromChain: route.fromChain,
toChain: route.toChain, toChain: route.toChain,
@@ -152,7 +155,7 @@ export async function bridgeFromStacks(
KnownTokenId.isStacksToken(route.fromToken) && KnownTokenId.isStacksToken(route.fromToken) &&
KnownTokenId.isBRC20Token(route.toToken) KnownTokenId.isBRC20Token(route.toToken)
) { ) {
return bridgeFromStacks_toMeta({ return bridgeFromStacks_toMeta(ctx, {
...info, ...info,
fromChain: route.fromChain, fromChain: route.fromChain,
toChain: route.toChain, toChain: route.toChain,
@@ -165,7 +168,7 @@ export async function bridgeFromStacks(
KnownTokenId.isStacksToken(route.fromToken) && KnownTokenId.isStacksToken(route.fromToken) &&
KnownTokenId.isRunesToken(route.toToken) KnownTokenId.isRunesToken(route.toToken)
) { ) {
return bridgeFromStacks_toMeta({ return bridgeFromStacks_toMeta(ctx, {
...info, ...info,
fromChain: route.fromChain, fromChain: route.fromChain,
toChain: route.toChain, toChain: route.toChain,
@@ -200,7 +203,7 @@ async function bridgeFromStacks_toBitcoin(
): Promise<BridgeFromStacksOutput> { ): Promise<BridgeFromStacksOutput> {
const contractCallInfo = getStacksContractCallInfo( const contractCallInfo = getStacksContractCallInfo(
info.fromChain, info.fromChain,
"btc-peg-out-endpoint-v2-01", StacksContractName.BTCPegOutEndpoint,
) )
if (!contractCallInfo) { if (!contractCallInfo) {
throw new UnsupportedBridgeRouteError( throw new UnsupportedBridgeRouteError(
@@ -223,13 +226,14 @@ async function bridgeFromStacks_toBitcoin(
"peg-out-address": addressToScriptPubKey(bitcoinNetwork, info.toAddress), "peg-out-address": addressToScriptPubKey(bitcoinNetwork, info.toAddress),
amount: numberToStacksContractNumber(info.amount), amount: numberToStacksContractNumber(info.amount),
}, },
{ deployerAddress: contractCallInfo.deployerAddress }, contractCallInfo.executeOptions,
) )
return await info.sendTransaction(options) return await info.sendTransaction(options)
} }
async function bridgeFromStacks_toEVM( async function bridgeFromStacks_toEVM(
ctx: SDKGlobalContext,
info: Omit< info: Omit<
BridgeFromStacksInput, BridgeFromStacksInput,
"fromChain" | "toChain" | "fromToken" | "toToken" "fromChain" | "toChain" | "fromToken" | "toToken"
@@ -238,9 +242,10 @@ async function bridgeFromStacks_toEVM(
): Promise<BridgeFromStacksOutput> { ): Promise<BridgeFromStacksOutput> {
const contractCallInfo = getStacksContractCallInfo( const contractCallInfo = getStacksContractCallInfo(
info.fromChain, info.fromChain,
"cross-peg-out-endpoint-v2-01", StacksContractName.EVMPegOutEndpoint,
) )
const fromTokenContractInfo = getStacksTokenContractInfo( const fromTokenContractInfo = await getStacksTokenContractInfo(
ctx,
info.fromChain, info.fromChain,
info.fromToken, info.fromToken,
) )
@@ -265,13 +270,14 @@ async function bridgeFromStacks_toEVM(
"dest-chain-id": contractAssignedChainIdFromKnownChain(info.toChain), "dest-chain-id": contractAssignedChainIdFromKnownChain(info.toChain),
"settle-address": decodeHex(info.toAddress), "settle-address": decodeHex(info.toAddress),
}, },
{ deployerAddress: contractCallInfo.deployerAddress }, contractCallInfo.executeOptions,
) )
return await info.sendTransaction(options) return await info.sendTransaction(options)
} }
async function bridgeFromStacks_toMeta( async function bridgeFromStacks_toMeta(
ctx: SDKGlobalContext,
info: Omit< info: Omit<
BridgeFromStacksInput, BridgeFromStacksInput,
"fromChain" | "toChain" | "fromToken" | "toToken" "fromChain" | "toChain" | "fromToken" | "toToken"
@@ -280,9 +286,10 @@ async function bridgeFromStacks_toMeta(
): Promise<BridgeFromStacksOutput> { ): Promise<BridgeFromStacksOutput> {
const contractCallInfo = getStacksContractCallInfo( const contractCallInfo = getStacksContractCallInfo(
info.fromChain, info.fromChain,
"meta-peg-out-endpoint-v2-04", StacksContractName.MetaPegOutEndpoint,
) )
const fromTokenContractInfo = getStacksTokenContractInfo( const fromTokenContractInfo = await getStacksTokenContractInfo(
ctx,
info.fromChain, info.fromChain,
info.fromToken, info.fromToken,
) )
@@ -310,7 +317,7 @@ async function bridgeFromStacks_toMeta(
"token-trait": `${fromTokenContractInfo.deployerAddress}.${fromTokenContractInfo.contractName}`, "token-trait": `${fromTokenContractInfo.deployerAddress}.${fromTokenContractInfo.contractName}`,
amount: numberToStacksContractNumber(info.amount), amount: numberToStacksContractNumber(info.amount),
}, },
{ deployerAddress: contractCallInfo.deployerAddress }, contractCallInfo.executeOptions,
) )
return await info.sendTransaction(options) return await info.sendTransaction(options)

View File

@@ -1,17 +1,26 @@
import { getBtc2StacksFeeInfo } from "../bitcoinUtils/peggingHelpers" import { getBtc2StacksFeeInfo } from "../bitcoinUtils/peggingHelpers"
import { getStacks2EvmFeeInfo } from "../evmUtils/peggingHelpers" import { getStacks2EvmFeeInfo } from "../evmUtils/peggingHelpers"
import { getStacks2MetaFeeInfo } from "../metaUtils/peggingHelpers"
import { BigNumber } from "../utils/BigNumber" import { BigNumber } from "../utils/BigNumber"
import {
getTransitStacksChainTransitStepInfos,
SwapRoute_WithExchangeRate_Public,
} from "../utils/SwapRouteHelpers"
import { import {
KnownRoute, KnownRoute,
KnownRoute_FromBitcoin_ToBRC20,
KnownRoute_FromBitcoin_ToEVM, KnownRoute_FromBitcoin_ToEVM,
KnownRoute_FromBitcoin_ToRunes,
KnownRoute_FromBitcoin_ToStacks, KnownRoute_FromBitcoin_ToStacks,
KnownRoute_FromStacks_ToBRC20,
KnownRoute_FromStacks_ToRunes,
} from "../utils/buildSupportedRoutes" } from "../utils/buildSupportedRoutes"
import { UnsupportedBridgeRouteError } from "../utils/errors" import { UnsupportedBridgeRouteError } from "../utils/errors"
import { assertExclude, checkNever } from "../utils/typeHelpers" import { assertExclude, checkNever } from "../utils/typeHelpers"
import { import {
PublicTransferProphetAggregated, PublicTransferProphetAggregated,
transformToPublicTransferProphet, transformToPublicTransferProphet,
transformToPublicTransferProphetAggregated, transformToPublicTransferProphetAggregated2,
} from "../utils/types/TransferProphet" } from "../utils/types/TransferProphet"
import { KnownChainId, KnownTokenId } from "../utils/types/knownIds" import { KnownChainId, KnownTokenId } from "../utils/types/knownIds"
import { supportedRoutes } from "./bridgeFromBitcoin" import { supportedRoutes } from "./bridgeFromBitcoin"
@@ -24,6 +33,7 @@ export interface BridgeInfoFromBitcoinInput {
fromToken: TokenId fromToken: TokenId
toToken: TokenId toToken: TokenId
amount: SDKNumber amount: SDKNumber
swapRoute?: SwapRoute_WithExchangeRate_Public
} }
export interface BridgeInfoFromBitcoinOutput export interface BridgeInfoFromBitcoinOutput
@@ -54,7 +64,33 @@ export const bridgeInfoFromBitcoin = async (
KnownTokenId.isBitcoinToken(route.fromToken) && KnownTokenId.isBitcoinToken(route.fromToken) &&
KnownTokenId.isEVMToken(route.toToken) 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, ...info,
fromChain: route.fromChain, fromChain: route.fromChain,
toChain: route.toChain, toChain: route.toChain,
@@ -62,13 +98,6 @@ export const bridgeInfoFromBitcoin = async (
toToken: route.toToken, toToken: route.toToken,
}) })
} }
} else if (
KnownChainId.isBRC20Chain(route.toChain) ||
KnownChainId.isRunesChain(route.toChain)
) {
assertExclude(route.toChain, assertExclude.i<KnownChainId.BRC20Chain>())
assertExclude(route.toChain, assertExclude.i<KnownChainId.RunesChain>())
// TODO: bitcoin to brc20/runes is not supported yet
} else { } else {
assertExclude(route.toChain, assertExclude.i<KnownChainId.BitcoinChain>()) assertExclude(route.toChain, assertExclude.i<KnownChainId.BitcoinChain>())
checkNever(route) checkNever(route)
@@ -94,7 +123,9 @@ async function bridgeInfoFromBitcoin_toStacks(
> & > &
KnownRoute_FromBitcoin_ToStacks, KnownRoute_FromBitcoin_ToStacks,
): Promise<BridgeInfoFromBitcoinOutput> { ): Promise<BridgeInfoFromBitcoinOutput> {
const step1 = await getBtc2StacksFeeInfo(info) const step1 = await getBtc2StacksFeeInfo(info, {
swapRoute: info.swapRoute ?? null,
})
if (step1 == null) { if (step1 == null) {
throw new UnsupportedBridgeRouteError( throw new UnsupportedBridgeRouteError(
info.fromChain, info.fromChain,
@@ -111,6 +142,7 @@ async function bridgeInfoFromBitcoin_toStacks(
} }
async function bridgeInfoFromBitcoin_toEVM( async function bridgeInfoFromBitcoin_toEVM(
ctx: SDKGlobalContext,
info: Omit< info: Omit<
BridgeInfoFromBitcoinInput, BridgeInfoFromBitcoinInput,
"fromChain" | "toChain" | "fromToken" | "toToken" "fromChain" | "toChain" | "fromToken" | "toToken"
@@ -135,9 +167,21 @@ async function bridgeInfoFromBitcoin_toEVM(
toToken: info.toToken, 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([ const [step1, step2] = await Promise.all([
getBtc2StacksFeeInfo(step1Route), getBtc2StacksFeeInfo(step1Route, {
getStacks2EvmFeeInfo(step2Route), swapRoute: info.swapRoute ?? null,
}),
getStacks2EvmFeeInfo(ctx, step2Route),
]) ])
if (step1 == null || step2 == null) { if (step1 == null || step2 == null) {
throw new UnsupportedBridgeRouteError( throw new UnsupportedBridgeRouteError(
@@ -148,22 +192,64 @@ async function bridgeInfoFromBitcoin_toEVM(
) )
} }
const step1TransferProphet = transformToPublicTransferProphet( return transformToPublicTransferProphetAggregated2(
step1Route, [step1Route, step2Route],
info.amount, [step1, step2],
step1, 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<BridgeInfoFromBitcoinOutput> {
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],
}
} }

View File

@@ -3,7 +3,7 @@ import { nativeCurrencyAddress } from "../evmUtils/addressHelpers"
import { import {
getEvm2StacksFeeInfo, getEvm2StacksFeeInfo,
getStacks2EvmFeeInfo, getStacks2EvmFeeInfo,
toCorrespondingStacksToken, evmTokenToCorrespondingStacksToken,
} from "../evmUtils/peggingHelpers" } from "../evmUtils/peggingHelpers"
import { getEVMTokenContractInfo } from "../evmUtils/xlinkContractHelpers" import { getEVMTokenContractInfo } from "../evmUtils/xlinkContractHelpers"
import { getStacks2MetaFeeInfo } from "../metaUtils/peggingHelpers" import { getStacks2MetaFeeInfo } from "../metaUtils/peggingHelpers"
@@ -23,7 +23,7 @@ import { assertExclude, checkNever } from "../utils/typeHelpers"
import { import {
PublicTransferProphetAggregated, PublicTransferProphetAggregated,
transformToPublicTransferProphet, transformToPublicTransferProphet,
transformToPublicTransferProphetAggregated, transformToPublicTransferProphetAggregated2,
} from "../utils/types/TransferProphet" } from "../utils/types/TransferProphet"
import { KnownChainId, KnownTokenId } from "../utils/types/knownIds" import { KnownChainId, KnownTokenId } from "../utils/types/knownIds"
import { supportedRoutes } from "./bridgeFromEVM" import { supportedRoutes } from "./bridgeFromEVM"
@@ -173,7 +173,9 @@ async function bridgeInfoFromEVM_toBitcoin(
const transitStacksChain = KnownChainId.isEVMMainnetChain(info.fromChain) const transitStacksChain = KnownChainId.isEVMMainnetChain(info.fromChain)
? KnownChainId.Stacks.Mainnet ? KnownChainId.Stacks.Mainnet
: KnownChainId.Stacks.Testnet : KnownChainId.Stacks.Testnet
const transitStacksToken = await toCorrespondingStacksToken(info.fromToken) const transitStacksToken = await evmTokenToCorrespondingStacksToken(
info.fromToken,
)
if (transitStacksToken == null) { if (transitStacksToken == null) {
throw new UnsupportedBridgeRouteError( throw new UnsupportedBridgeRouteError(
info.fromChain, info.fromChain,
@@ -198,7 +200,9 @@ async function bridgeInfoFromEVM_toBitcoin(
const [step1, step2] = await Promise.all([ const [step1, step2] = await Promise.all([
getEvm2StacksFeeInfo(ctx, step1Route), getEvm2StacksFeeInfo(ctx, step1Route),
getStacks2BtcFeeInfo(step2Route), getStacks2BtcFeeInfo(step2Route, {
swappedFromRoute: step1Route,
}),
]) ])
if (step1 == null || step2 == null) { if (step1 == null || step2 == null) {
throw new UnsupportedBridgeRouteError( throw new UnsupportedBridgeRouteError(
@@ -209,24 +213,12 @@ async function bridgeInfoFromEVM_toBitcoin(
) )
} }
const step1TransferProphet = transformToPublicTransferProphet( return transformToPublicTransferProphetAggregated2(
step1Route, [step1Route, step2Route],
info.amount, [step1, step2],
step1, 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( async function bridgeInfoFromEVM_toEVM(
@@ -246,7 +238,9 @@ async function bridgeInfoFromEVM_toEVM(
const transitStacksChain = KnownChainId.isEVMMainnetChain(info.fromChain) const transitStacksChain = KnownChainId.isEVMMainnetChain(info.fromChain)
? KnownChainId.Stacks.Mainnet ? KnownChainId.Stacks.Mainnet
: KnownChainId.Stacks.Testnet : KnownChainId.Stacks.Testnet
const transitStacksToken = await toCorrespondingStacksToken(info.fromToken) const transitStacksToken = await evmTokenToCorrespondingStacksToken(
info.fromToken,
)
if ( if (
transitStacksToken == null || transitStacksToken == null ||
evmTokenContractCallInfo?.tokenContractAddress === nativeCurrencyAddress evmTokenContractCallInfo?.tokenContractAddress === nativeCurrencyAddress
@@ -274,7 +268,7 @@ async function bridgeInfoFromEVM_toEVM(
const [step1, step2] = await Promise.all([ const [step1, step2] = await Promise.all([
getEvm2StacksFeeInfo(ctx, step1Route), getEvm2StacksFeeInfo(ctx, step1Route),
getStacks2EvmFeeInfo(step2Route), getStacks2EvmFeeInfo(ctx, step2Route),
]) ])
if (step1 == null || step2 == null) { if (step1 == null || step2 == null) {
throw new UnsupportedBridgeRouteError( throw new UnsupportedBridgeRouteError(
@@ -285,24 +279,12 @@ async function bridgeInfoFromEVM_toEVM(
) )
} }
const step1TransferProphet = transformToPublicTransferProphet( return transformToPublicTransferProphetAggregated2(
step1Route, [step1Route, step2Route],
info.amount, [step1, step2],
step1, 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( async function bridgeInfoFromEVM_toMeta(
@@ -322,7 +304,9 @@ async function bridgeInfoFromEVM_toMeta(
const transitStacksChain = KnownChainId.isEVMMainnetChain(info.fromChain) const transitStacksChain = KnownChainId.isEVMMainnetChain(info.fromChain)
? KnownChainId.Stacks.Mainnet ? KnownChainId.Stacks.Mainnet
: KnownChainId.Stacks.Testnet : KnownChainId.Stacks.Testnet
const transitStacksToken = await toCorrespondingStacksToken(info.fromToken) const transitStacksToken = await evmTokenToCorrespondingStacksToken(
info.fromToken,
)
if ( if (
transitStacksToken == null || transitStacksToken == null ||
evmTokenContractCallInfo?.tokenContractAddress === nativeCurrencyAddress evmTokenContractCallInfo?.tokenContractAddress === nativeCurrencyAddress
@@ -363,22 +347,10 @@ async function bridgeInfoFromEVM_toMeta(
) )
} }
const step1TransferProphet = transformToPublicTransferProphet( return transformToPublicTransferProphetAggregated2(
step1Route, [step1Route, step2Route],
info.amount, [step1, step2],
step1, BigNumber.from(info.amount),
BigNumber.ONE,
) )
const step2TransferProphet = transformToPublicTransferProphet(
step2Route,
step1TransferProphet.toAmount,
step2,
)
return {
...transformToPublicTransferProphetAggregated(
[step1TransferProphet, step2TransferProphet],
[BigNumber.ONE],
),
transferProphets: [step1TransferProphet, step2TransferProphet],
}
} }

View File

@@ -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<BridgeInfoFromMetaOutput> => {
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<KnownChainId.StacksChain>())
assertExclude(route.fromChain, assertExclude.i<KnownChainId.EVMChain>())
assertExclude(route.fromChain, assertExclude.i<KnownChainId.BitcoinChain>())
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<BridgeInfoFromMetaOutput> {
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<BridgeInfoFromMetaOutput> {
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<BridgeInfoFromMetaOutput> {
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<BridgeInfoFromMetaOutput> {
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),
)
}

View File

@@ -54,7 +54,7 @@ export async function bridgeInfoFromStacks(
KnownTokenId.isStacksToken(route.fromToken) && KnownTokenId.isStacksToken(route.fromToken) &&
KnownTokenId.isEVMToken(route.toToken) KnownTokenId.isEVMToken(route.toToken)
) { ) {
return bridgeInfoFromStacks_toEVM({ return bridgeInfoFromStacks_toEVM(ctx, {
...info, ...info,
fromChain: route.fromChain, fromChain: route.fromChain,
toChain: route.toChain, toChain: route.toChain,
@@ -113,7 +113,9 @@ async function bridgeInfoFromStacks_toBitcoin(
> & > &
KnownRoute_FromStacks_ToBitcoin, KnownRoute_FromStacks_ToBitcoin,
): Promise<BridgeInfoFromStacksOutput> { ): Promise<BridgeInfoFromStacksOutput> {
const step1 = await getStacks2BtcFeeInfo(info) const step1 = await getStacks2BtcFeeInfo(info, {
swappedFromRoute: null,
})
if (step1 == null) { if (step1 == null) {
throw new UnsupportedBridgeRouteError( throw new UnsupportedBridgeRouteError(
info.fromChain, info.fromChain,
@@ -130,13 +132,14 @@ async function bridgeInfoFromStacks_toBitcoin(
} }
async function bridgeInfoFromStacks_toEVM( async function bridgeInfoFromStacks_toEVM(
ctx: SDKGlobalContext,
info: Omit< info: Omit<
BridgeInfoFromStacksInput, BridgeInfoFromStacksInput,
"fromChain" | "toChain" | "fromToken" | "toToken" "fromChain" | "toChain" | "fromToken" | "toToken"
> & > &
KnownRoute_FromStacks_ToEVM, KnownRoute_FromStacks_ToEVM,
): Promise<BridgeInfoFromStacksOutput> { ): Promise<BridgeInfoFromStacksOutput> {
const step1 = await getStacks2EvmFeeInfo(info) const step1 = await getStacks2EvmFeeInfo(ctx, info)
if (step1 == null) { if (step1 == null) {
throw new UnsupportedBridgeRouteError( throw new UnsupportedBridgeRouteError(
info.fromChain, info.fromChain,

View File

@@ -2,14 +2,29 @@ import { getBTCPegInAddress } from "../bitcoinUtils/btcAddresses"
import { ReselectSpendableUTXOsFn } from "../bitcoinUtils/prepareTransaction" import { ReselectSpendableUTXOsFn } from "../bitcoinUtils/prepareTransaction"
import { import {
createBridgeOrder_BitcoinToEVM, createBridgeOrder_BitcoinToEVM,
createBridgeOrder_BitcoinToMeta,
createBridgeOrder_BitcoinToStacks, createBridgeOrder_BitcoinToStacks,
} from "../stacksUtils/createBridgeOrder" } from "../stacksUtils/createBridgeOrderFromBitcoin"
import { numberToStacksContractNumber } from "../stacksUtils/xlinkContractHelpers" import { BigNumber } from "../utils/BigNumber"
import { UnsupportedBridgeRouteError } from "../utils/errors" 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 { 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 { prepareBitcoinTransaction, supportedRoutes } from "./bridgeFromBitcoin"
import { ChainId, SDKNumber, TokenId, toSDKNumberOrUndefined } from "./types" import { ChainId, SDKNumber, TokenId, toSDKNumberOrUndefined } from "./types"
import { SwapRoute_WithMinimumAmountsToReceive_Public } from "../utils/SwapRouteHelpers"
import { SDKGlobalContext } from "./types.internal" import { SDKGlobalContext } from "./types.internal"
export interface EstimateBridgeTransactionFromBitcoinInput { export interface EstimateBridgeTransactionFromBitcoinInput {
@@ -20,7 +35,12 @@ export interface EstimateBridgeTransactionFromBitcoinInput {
fromAddress: string fromAddress: string
fromAddressScriptPubKey: Uint8Array fromAddressScriptPubKey: Uint8Array
toAddress: string toAddress: string
/**
* **Required** when `toChain` is one of bitcoin chains
*/
toAddressScriptPubKey?: Uint8Array
amount: SDKNumber amount: SDKNumber
swapRoute?: SwapRoute_WithMinimumAmountsToReceive_Public
networkFeeRate: bigint networkFeeRate: bigint
reselectSpendableUTXOs: ReselectSpendableUTXOsFn reselectSpendableUTXOs: ReselectSpendableUTXOsFn
} }
@@ -63,13 +83,32 @@ export async function estimateBridgeTransactionFromBitcoin(
toToken: route.toToken, toToken: route.toToken,
}) })
} }
} else if ( } else if (KnownChainId.isBRC20Chain(route.toChain)) {
KnownChainId.isBRC20Chain(route.toChain) || if (
KnownChainId.isRunesChain(route.toChain) KnownTokenId.isBitcoinToken(route.fromToken) &&
) { KnownTokenId.isBRC20Token(route.toToken)
assertExclude(route.toChain, assertExclude.i<KnownChainId.BRC20Chain>()) ) {
assertExclude(route.toChain, assertExclude.i<KnownChainId.RunesChain>()) return estimateFromBitcoin_toMeta(ctx, {
// TODO: bitcoin to brc20/runes is not supported yet ...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 { } else {
assertExclude(route.toChain, assertExclude.i<KnownChainId.BitcoinChain>()) assertExclude(route.toChain, assertExclude.i<KnownChainId.BitcoinChain>())
checkNever(route) checkNever(route)
@@ -89,16 +128,12 @@ export async function estimateBridgeTransactionFromBitcoin(
} }
async function estimateFromBitcoin_toStacks( async function estimateFromBitcoin_toStacks(
sdkContext: Pick<SDKGlobalContext, "backendAPI">, sdkContext: SDKGlobalContext,
info: Omit< info: Omit<
EstimateBridgeTransactionFromBitcoinInput, EstimateBridgeTransactionFromBitcoinInput,
"fromChain" | "toChain" | "fromToken" | "toToken" "fromChain" | "toChain" | "fromToken" | "toToken"
> & { > &
fromChain: KnownChainId.BitcoinChain KnownRoute_FromBitcoin_ToStacks,
toChain: KnownChainId.StacksChain
fromToken: KnownTokenId.BitcoinToken
toToken: KnownTokenId.StacksToken
},
): Promise<EstimateBridgeTransactionFromBitcoinOutput> { ): Promise<EstimateBridgeTransactionFromBitcoinOutput> {
const pegInAddress = getBTCPegInAddress(info.fromChain, info.toChain) const pegInAddress = getBTCPegInAddress(info.fromChain, info.toChain)
if (pegInAddress == null) { 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, fromChain: info.fromChain,
fromBitcoinScriptPubKey: info.fromAddressScriptPubKey, fromBitcoinScriptPubKey: info.fromAddressScriptPubKey,
toChain: info.toChain, toChain: info.toChain,
toToken: info.toToken, toToken: info.toToken,
toStacksAddress: info.toAddress, toStacksAddress: info.toAddress,
swapSlippedAmount: numberToStacksContractNumber(info.amount), swap:
swapRoute: [], info.swapRoute == null
? undefined
: {
...info.swapRoute,
minimumAmountsToReceive: BigNumber.from(
info.swapRoute.minimumAmountsToReceive,
),
},
}) })
if (createdOrder == null) { if (createdOrder == null) {
throw new UnsupportedBridgeRouteError( throw new UnsupportedBridgeRouteError(
@@ -139,16 +181,12 @@ async function estimateFromBitcoin_toStacks(
} }
async function estimateFromBitcoin_toEVM( async function estimateFromBitcoin_toEVM(
sdkContext: Pick<SDKGlobalContext, "backendAPI">, sdkContext: SDKGlobalContext,
info: Omit< info: Omit<
EstimateBridgeTransactionFromBitcoinInput, EstimateBridgeTransactionFromBitcoinInput,
"fromChain" | "toChain" | "fromToken" | "toToken" "fromChain" | "toChain" | "fromToken" | "toToken"
> & { > &
fromChain: KnownChainId.BitcoinChain KnownRoute_FromBitcoin_ToEVM,
toChain: KnownChainId.EVMChain
fromToken: KnownTokenId.BitcoinToken
toToken: KnownTokenId.EVMToken
},
): Promise<EstimateBridgeTransactionFromBitcoinOutput> { ): Promise<EstimateBridgeTransactionFromBitcoinOutput> {
const pegInAddress = getBTCPegInAddress(info.fromChain, info.toChain) const pegInAddress = getBTCPegInAddress(info.fromChain, info.toChain)
if (pegInAddress == null) { 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, fromChain: info.fromChain,
toChain: info.toChain, toChain: info.toChain,
toToken: info.toToken, toToken: info.toToken,
fromBitcoinScriptPubKey: info.fromAddressScriptPubKey, fromBitcoinScriptPubKey: info.fromAddressScriptPubKey,
toEVMAddress: info.toAddress, toEVMAddress: info.toAddress,
swapSlippedAmount: numberToStacksContractNumber(info.amount), swap:
swapRoute: [], 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<EstimateBridgeTransactionFromBitcoinOutput> {
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) { if (createdOrder == null) {
throw new UnsupportedBridgeRouteError( throw new UnsupportedBridgeRouteError(

View File

@@ -1,10 +1,12 @@
import { Client } from "viem" import { Client } from "viem"
import { EVMOnChainAddresses } from "../evmUtils/evmContractAddresses" import { EVMOnChainAddresses } from "../evmUtils/evmContractAddresses"
import { KnownChainId } from "../utils/types/knownIds"
import type { import type {
BRC20SupportedRoute, BRC20SupportedRoute,
RunesSupportedRoute, RunesSupportedRoute,
} from "../metaUtils/xlinkContractHelpers" } from "../metaUtils/xlinkContractHelpers"
import { StacksTokenInfo } from "../stacksUtils/xlinkContractHelpers"
import { KnownChainId } from "../utils/types/knownIds"
import { EVMAddress } from "./types"
export interface SDKGlobalContextCache<K, T> { export interface SDKGlobalContextCache<K, T> {
get: (key: K) => T | null get: (key: K) => T | null
@@ -16,22 +18,33 @@ export interface SDKGlobalContext {
backendAPI: { backendAPI: {
runtimeEnv: "prod" | "dev" runtimeEnv: "prod" | "dev"
} }
stacks: {
tokensCache?: SDKGlobalContextCache<
KnownChainId.StacksChain,
Promise<StacksTokenInfo[]>
>
}
btc: {
ignoreValidateResult?: boolean
}
brc20: { brc20: {
routesConfigCache?: SDKGlobalContextCache< routesConfigCache?: SDKGlobalContextCache<
string, KnownChainId.BRC20Chain,
Promise<BRC20SupportedRoute[]> Promise<BRC20SupportedRoute[]>
> >
ignoreValidateResult?: boolean
} }
runes: { runes: {
routesConfigCache?: SDKGlobalContextCache< routesConfigCache?: SDKGlobalContextCache<
string, KnownChainId.RunesChain,
Promise<RunesSupportedRoute[]> Promise<RunesSupportedRoute[]>
> >
ignoreValidateResult?: boolean
} }
evm: { evm: {
enableMulticall?: boolean enableMulticall?: boolean
onChainConfigCache?: SDKGlobalContextCache< onChainConfigCache?: SDKGlobalContextCache<
string, `${KnownChainId.EVMChain}:${EVMAddress}`,
Promise<EVMOnChainAddresses> Promise<EVMOnChainAddresses>
> >
viemClients: Partial<Record<KnownChainId.EVMChain, Client>> viemClients: Partial<Record<KnownChainId.EVMChain, Client>>

12
tsup.config.ts Normal file
View File

@@ -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",
},
})