initial commit

This commit is contained in:
c4605
2024-04-13 00:14:51 +01:00
commit ed781f7156
54 changed files with 8176 additions and 0 deletions

2
.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
lib
generated/smartContract

11
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,11 @@
module.exports = {
root: true,
extends: [
"./node_modules/@c4605/toolconfs/eslintrc.base",
"./node_modules/@c4605/toolconfs/eslintrc.prettier",
"./node_modules/@c4605/toolconfs/eslintrc.ts",
],
parserOptions: {
project: './tsconfig.json',
},
}

24
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Tests
on: [push, pull_request]
jobs:
test:
name: Unit test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Enable corepack
run: corepack enable
- name: Install dependencies
run: pnpm install
- name: Test
run: pnpm test

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
lib/

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
generated/smartContract

4
.prettierrc.cjs Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
...require("@c4605/toolconfs/prettierrc"),
singleQuote: false,
}

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

1
README.md Normal file
View File

@@ -0,0 +1 @@
# xlink-sdk

View File

@@ -0,0 +1,536 @@
import {
defineContract,
uintT,
bufferT,
responseSimpleT,
booleanT,
tupleT,
listT,
traitT,
optionalT,
principalT,
noneT
} from "../smartContractHelpers/codegenImport"
export const btcBridgeEndpointV111 = defineContract({
"btc-bridge-endpoint-v1-11": {
'claim-peg-out': {
input: [
{ name: 'request-id', type: uintT },
{ name: 'fulfilled-by', type: bufferT }
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'finalize-peg-in-0': {
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: 'order-idx', type: uintT }
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'finalize-peg-in-1': {
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: 'order-idx', type: uintT },
{ name: 'token-trait', type: traitT }
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'finalize-peg-in-2': {
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: 'order-idx', type: uintT },
{ name: 'token1-trait', type: traitT },
{ name: 'token2-trait', type: traitT }
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'finalize-peg-in-3': {
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: 'order-idx', type: uintT },
{ name: 'token1-trait', type: traitT },
{ name: 'token2-trait', type: traitT },
{ name: 'token3-trait', type: traitT }
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'finalize-peg-in-launchpad': {
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: 'order-idx', type: uintT },
{ name: 'owner-idx', type: uintT }
],
output: responseSimpleT(tupleT({ end: uintT, start: uintT }, ), ),
mode: 'public'
},
'finalize-peg-out': {
input: [
{ name: 'request-id', type: uintT },
{ 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: 'fulfilled-by-idx', type: uintT }
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'pause-peg-in': {
input: [ { name: 'paused', type: booleanT } ],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'pause-peg-out': {
input: [ { name: 'paused', type: booleanT } ],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'request-peg-out-0': {
input: [
{ name: 'peg-out-address', type: bufferT },
{ name: 'amount', type: uintT }
],
output: responseSimpleT(uintT, ),
mode: 'public'
},
'request-peg-out-1': {
input: [
{ name: 'peg-out-address', type: bufferT },
{ name: 'token-trait', type: traitT },
{ name: 'factor', type: uintT },
{ name: 'dx', type: uintT },
{ name: 'min-dy', type: optionalT(uintT, ) }
],
output: responseSimpleT(uintT, ),
mode: 'public'
},
'request-peg-out-2': {
input: [
{ name: 'peg-out-address', type: bufferT },
{ name: 'token1-trait', type: traitT },
{ name: 'token2-trait', type: traitT },
{ name: 'factor1', type: uintT },
{ name: 'factor2', type: uintT },
{ name: 'dx', type: uintT },
{ name: 'min-dz', type: optionalT(uintT, ) }
],
output: responseSimpleT(uintT, ),
mode: 'public'
},
'request-peg-out-3': {
input: [
{ name: 'peg-out-address', type: bufferT },
{ name: 'token1-trait', type: traitT },
{ name: 'token2-trait', type: traitT },
{ name: 'token3-trait', type: traitT },
{ name: 'factor1', type: uintT },
{ name: 'factor2', type: uintT },
{ name: 'factor3', type: uintT },
{ name: 'dx', type: uintT },
{ name: 'min-dw', type: optionalT(uintT, ) }
],
output: responseSimpleT(uintT, ),
mode: 'public'
},
'revoke-peg-out': {
input: [ { name: 'request-id', type: uintT } ],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'set-contract-owner': {
input: [ { name: 'new-contract-owner', type: principalT } ],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'set-fee-address': {
input: [ { name: 'new-fee-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'
},
'set-peg-out-fee': {
input: [ { name: 'fee', type: uintT } ],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'set-peg-out-min-fee': {
input: [ { name: 'fee', type: uintT } ],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'set-use-whitelist': {
input: [
{ name: 'launch-id', type: uintT },
{ name: 'new-whitelisted', type: booleanT }
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'set-whitelisted': {
input: [
{ name: 'launch-id', type: uintT },
{
name: 'whitelisted-users',
type: listT(tupleT({ owner: bufferT, whitelisted: booleanT }, ), )
}
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'create-order-0-or-fail': {
input: [ { name: 'order', type: principalT } ],
output: responseSimpleT(bufferT, ),
mode: 'readonly'
},
'create-order-1-or-fail': {
input: [
{
name: 'order',
type: tupleT({ 'min-dy': uintT, 'pool-id': uintT, user: principalT }, )
}
],
output: responseSimpleT(bufferT, ),
mode: 'readonly'
},
'create-order-2-or-fail': {
input: [
{
name: 'order',
type: tupleT({
'min-dz': uintT,
'pool1-id': uintT,
'pool2-id': uintT,
user: principalT
}, )
}
],
output: responseSimpleT(bufferT, ),
mode: 'readonly'
},
'create-order-3-or-fail': {
input: [
{
name: 'order',
type: tupleT({
'min-dw': uintT,
'pool1-id': uintT,
'pool2-id': uintT,
'pool3-id': uintT,
user: principalT
}, )
}
],
output: responseSimpleT(bufferT, ),
mode: 'readonly'
},
'create-order-launchpad-or-fail': {
input: [
{
name: 'order',
type: tupleT({ 'launch-id': uintT, user: principalT }, )
}
],
output: responseSimpleT(bufferT, ),
mode: 'readonly'
},
'decode-order-0-or-fail': {
input: [ { name: 'order-script', type: bufferT } ],
output: responseSimpleT(principalT, ),
mode: 'readonly'
},
'decode-order-1-or-fail': {
input: [ { name: 'order-script', type: bufferT } ],
output: responseSimpleT(tupleT({ 'min-dy': uintT, 'pool-id': uintT, user: principalT }, ), ),
mode: 'readonly'
},
'decode-order-2-or-fail': {
input: [ { name: 'order-script', type: bufferT } ],
output: responseSimpleT(tupleT({
'min-dz': uintT,
'pool1-id': uintT,
'pool2-id': uintT,
user: principalT
}, ), ),
mode: 'readonly'
},
'decode-order-3-or-fail': {
input: [ { name: 'order-script', type: bufferT } ],
output: responseSimpleT(tupleT({
'min-dw': uintT,
'pool1-id': uintT,
'pool2-id': uintT,
'pool3-id': uintT,
user: principalT
}, ), ),
mode: 'readonly'
},
'decode-order-launchpad-or-fail': {
input: [ { name: 'order-script', type: bufferT } ],
output: responseSimpleT(tupleT({ 'launch-id': uintT, user: principalT }, ), ),
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-fee-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-peg-out-fee': { input: [], output: uintT, mode: 'readonly' },
'get-peg-out-min-fee': { input: [], output: uintT, mode: 'readonly' },
'get-request-claim-grace-period': { input: [], output: uintT, mode: 'readonly' },
'get-request-or-fail': {
input: [ { name: 'request-id', type: uintT } ],
output: responseSimpleT(tupleT({
'amount-net': uintT,
claimed: uintT,
'claimed-by': principalT,
fee: uintT,
finalized: booleanT,
'fulfilled-by': bufferT,
'gas-fee': uintT,
'peg-out-address': bufferT,
'requested-at': uintT,
'requested-at-burn-height': uintT,
'requested-by': principalT,
revoked: booleanT
}, ), ),
mode: 'readonly'
},
'get-request-revoke-grace-period': { input: [], output: uintT, mode: 'readonly' },
'get-txid': {
input: [ { name: 'tx', type: bufferT } ],
output: responseSimpleT(bufferT, ),
mode: 'readonly'
},
'get-use-whitelist-or-default': {
input: [ { name: 'launch-id', type: uintT } ],
output: booleanT,
mode: 'readonly'
},
'get-whitelisted-or-default': {
input: [
{ name: 'launch-id', type: uintT },
{ name: 'owner', type: bufferT }
],
output: 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' },
'is-peg-out-paused': { input: [], output: booleanT, mode: 'readonly' },
'validate-tx-0': {
input: [
{ name: 'tx', type: bufferT },
{ name: 'output-idx', type: uintT },
{ name: 'order-idx', type: uintT }
],
output: responseSimpleT(tupleT({ 'amount-net': uintT, fee: uintT, 'order-details': principalT }, ), ),
mode: 'readonly'
},
'validate-tx-1': {
input: [
{ name: 'tx', type: bufferT },
{ name: 'output-idx', type: uintT },
{ name: 'order-idx', type: uintT },
{ name: 'token', type: principalT }
],
output: responseSimpleT(tupleT({
factor: uintT,
'token-y': principalT,
'validation-data': tupleT({
'amount-net': uintT,
fee: uintT,
'order-details': tupleT({ 'min-dy': uintT, 'pool-id': uintT, user: principalT }, )
}, )
}, ), ),
mode: 'readonly'
},
'validate-tx-2': {
input: [
{ name: 'tx', type: bufferT },
{ name: 'output-idx', type: uintT },
{ name: 'order-idx', type: uintT },
{ name: 'token1', type: principalT },
{ name: 'token2', type: principalT }
],
output: responseSimpleT(tupleT({
factor1: uintT,
factor2: uintT,
'token1-y': principalT,
'token2-y': principalT,
'validation-data': tupleT({
'amount-net': uintT,
fee: uintT,
'order-details': tupleT({
'min-dz': uintT,
'pool1-id': uintT,
'pool2-id': uintT,
user: principalT
}, )
}, )
}, ), ),
mode: 'readonly'
},
'validate-tx-3': {
input: [
{ name: 'tx', type: bufferT },
{ name: 'output-idx', type: uintT },
{ name: 'order-idx', type: uintT },
{ name: 'token1', type: principalT },
{ name: 'token2', type: principalT },
{ name: 'token3', type: principalT }
],
output: responseSimpleT(tupleT({
factor1: uintT,
factor2: uintT,
factor3: uintT,
'token1-y': principalT,
'token2-y': principalT,
'token3-y': principalT,
'validation-data': tupleT({
'amount-net': uintT,
fee: uintT,
'order-details': tupleT({
'min-dw': uintT,
'pool1-id': uintT,
'pool2-id': uintT,
'pool3-id': uintT,
user: principalT
}, )
}, )
}, ), ),
mode: 'readonly'
},
'validate-tx-launchpad': {
input: [
{ name: 'tx', type: bufferT },
{ name: 'output-idx', type: uintT },
{ name: 'order-idx', type: uintT },
{ name: 'owner-idx', type: uintT }
],
output: responseSimpleT(tupleT({
'amount-net': uintT,
fee: uintT,
'order-details': tupleT({ 'launch-id': uintT, user: principalT }, ),
'owner-script': bufferT
}, ), ),
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'
},
'use-whitelist': { input: uintT, output: optionalT(booleanT, ), mode: 'mapEntry' },
whitelisted: {
input: tupleT({ 'launch-id': uintT, owner: bufferT }, ),
output: optionalT(booleanT, ),
mode: 'mapEntry'
},
'contract-owner': { input: noneT, output: principalT, mode: 'variable' },
'fee-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' },
'peg-out-fee': { input: noneT, output: uintT, mode: 'variable' },
'peg-out-min-fee': { input: noneT, output: uintT, mode: 'variable' },
'peg-out-paused': { input: noneT, output: booleanT, mode: 'variable' }
}
} as const)

View File

@@ -0,0 +1,421 @@
import {
defineContract,
booleanT,
responseSimpleT,
principalT,
uintT,
listT,
tupleT,
bufferT,
traitT,
stringT,
optionalT,
noneT
} from "../smartContractHelpers/codegenImport"
export const crossBridgeEndpointV103 = defineContract({
"cross-bridge-endpoint-v1-03": {
'apply-whitelist': {
input: [ { name: 'new-use-whitelist', type: booleanT } ],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'register-user': {
input: [ { name: 'user', type: principalT } ],
output: responseSimpleT(uintT, ),
mode: 'public'
},
'register-user-many': {
input: [ { name: 'users', type: listT(principalT, ) } ],
output: responseSimpleT(listT(responseSimpleT(uintT, ), ), ),
mode: 'public'
},
'set-contract-owner': {
input: [ { name: 'owner', type: principalT } ],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'set-launch-whitelisted': {
input: [
{ name: 'launch-id', type: uintT },
{
name: 'whitelisted',
type: listT(tupleT({ owner: bufferT, whitelisted: booleanT }, ), )
}
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'set-paused': {
input: [ { name: 'paused', type: booleanT } ],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'set-use-launch-whitelist': {
input: [
{ name: 'launch-id', type: uintT },
{ name: 'new-whitelisted', type: booleanT }
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'transfer-to-launchpad': {
input: [
{
name: 'order',
type: tupleT({
'amount-in-fixed': uintT,
'chain-id': uintT,
from: bufferT,
'launch-id': uintT,
salt: bufferT,
to: uintT,
token: uintT
}, )
},
{ name: 'token-trait', type: traitT },
{
name: 'signature-packs',
type: listT(tupleT({ 'order-hash': bufferT, signature: bufferT, signer: principalT }, ), )
}
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'transfer-to-unwrap': {
input: [
{ name: 'token-trait', type: traitT },
{ name: 'amount-in-fixed', type: uintT },
{ name: 'the-chain-id', type: uintT },
{ name: 'settle-address', type: bufferT }
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'transfer-to-wrap': {
input: [
{
name: 'order',
type: tupleT({
'amount-in-fixed': uintT,
'chain-id': uintT,
salt: bufferT,
to: uintT,
token: uintT
}, )
},
{ name: 'token-trait', type: traitT },
{
name: 'signature-packs',
type: listT(tupleT({ 'order-hash': bufferT, signature: bufferT, signer: principalT }, ), )
}
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
whitelist: {
input: [
{ name: 'user', type: principalT },
{ name: 'whitelisted', type: booleanT }
],
output: responseSimpleT(booleanT, ),
mode: 'public'
},
'whitelist-many': {
input: [
{ name: 'users', type: listT(principalT, ) },
{ name: 'whitelisted', type: listT(booleanT, ) }
],
output: responseSimpleT(listT(responseSimpleT(booleanT, ), ), ),
mode: 'public'
},
'check-is-approved-token': {
input: [ { name: 'token', type: principalT } ],
output: responseSimpleT(booleanT, ),
mode: 'readonly'
},
'create-launchpad-order': {
input: [
{
name: 'order',
type: tupleT({
'amount-in-fixed': uintT,
'chain-id': uintT,
from: bufferT,
'launch-id': uintT,
salt: bufferT,
to: uintT,
token: uintT
}, )
}
],
output: responseSimpleT(bufferT, ),
mode: 'readonly'
},
'create-wrap-order': {
input: [
{
name: 'order',
type: tupleT({
'amount-in-fixed': uintT,
'chain-id': uintT,
salt: bufferT,
to: uintT,
token: uintT
}, )
}
],
output: responseSimpleT(bufferT, ),
mode: 'readonly'
},
'decode-launchpad-order': {
input: [ { name: 'order-buff', type: bufferT } ],
output: responseSimpleT(tupleT({
'amount-in-fixed': uintT,
'chain-id': uintT,
from: bufferT,
'launch-id': uintT,
salt: bufferT,
to: uintT,
token: uintT
}, ), ),
mode: 'readonly'
},
'decode-wrap-order': {
input: [ { name: 'order-buff', type: bufferT } ],
output: responseSimpleT(tupleT({
'amount-in-fixed': uintT,
'chain-id': uintT,
salt: bufferT,
to: uintT,
token: uintT
}, ), ),
mode: 'readonly'
},
'get-approved-chain-or-fail': {
input: [ { name: 'the-chain-id', type: uintT } ],
output: responseSimpleT(tupleT({ 'buff-length': uintT, name: stringT }, ), ),
mode: 'readonly'
},
'get-approved-token-by-id-or-fail': {
input: [ { name: 'token-id', type: uintT } ],
output: responseSimpleT(tupleT({
'accrued-fee': uintT,
approved: booleanT,
burnable: booleanT,
fee: uintT,
'max-amount': uintT,
'min-amount': uintT,
token: principalT
}, ), ),
mode: 'readonly'
},
'get-approved-token-id-or-fail': {
input: [ { name: 'token', type: principalT } ],
output: responseSimpleT(uintT, ),
mode: 'readonly'
},
'get-approved-token-or-fail': {
input: [ { name: 'token', type: principalT } ],
output: responseSimpleT(tupleT({
'accrued-fee': uintT,
approved: booleanT,
burnable: booleanT,
fee: uintT,
'max-amount': uintT,
'min-amount': uintT,
token: principalT
}, ), ),
mode: 'readonly'
},
'get-contract-owner': {
input: [],
output: responseSimpleT(principalT, ),
mode: 'readonly'
},
'get-launch-whitelisted-or-default': {
input: [
{ name: 'launch-id', type: uintT },
{ name: 'owner', type: bufferT }
],
output: booleanT,
mode: 'readonly'
},
'get-min-fee-or-default': {
input: [
{ name: 'the-token-id', type: uintT },
{ name: 'the-chain-id', type: uintT }
],
output: uintT,
mode: 'readonly'
},
'get-paused': { input: [], output: booleanT, mode: 'readonly' },
'get-required-validators': { input: [], output: uintT, mode: 'readonly' },
'get-token-reserve-or-default': {
input: [
{ name: 'the-token-id', type: uintT },
{ name: 'the-chain-id', type: uintT }
],
output: uintT,
mode: 'readonly'
},
'get-use-launch-whitelist-or-default': {
input: [ { name: 'launch-id', type: uintT } ],
output: booleanT,
mode: 'readonly'
},
'get-use-whitelist': { input: [], output: booleanT, mode: 'readonly' },
'get-user-id': {
input: [ { name: 'user', type: principalT } ],
output: optionalT(uintT, ),
mode: 'readonly'
},
'get-user-id-or-fail': {
input: [ { name: 'user', type: principalT } ],
output: responseSimpleT(uintT, ),
mode: 'readonly'
},
'get-validator-id': {
input: [ { name: 'validator', type: principalT } ],
output: optionalT(uintT, ),
mode: 'readonly'
},
'get-validator-id-or-fail': {
input: [ { name: 'validator', type: principalT } ],
output: responseSimpleT(uintT, ),
mode: 'readonly'
},
'hash-launchpad-order': {
input: [
{
name: 'order',
type: tupleT({
'amount-in-fixed': uintT,
'chain-id': uintT,
from: bufferT,
'launch-id': uintT,
salt: bufferT,
to: uintT,
token: uintT
}, )
}
],
output: responseSimpleT(bufferT, ),
mode: 'readonly'
},
'hash-wrap-order': {
input: [
{
name: 'order',
type: tupleT({
'amount-in-fixed': uintT,
'chain-id': uintT,
salt: bufferT,
to: uintT,
token: uintT
}, )
}
],
output: responseSimpleT(bufferT, ),
mode: 'readonly'
},
'is-approved-operator-or-default': {
input: [ { name: 'operator', type: principalT } ],
output: booleanT,
mode: 'readonly'
},
'is-approved-relayer-or-default': {
input: [ { name: 'relayer', type: principalT } ],
output: booleanT,
mode: 'readonly'
},
'is-order-sent-or-default': {
input: [ { name: 'order-hash', type: bufferT } ],
output: booleanT,
mode: 'readonly'
},
'is-order-validated-by-or-default': {
input: [
{ name: 'order-hash', type: bufferT },
{ name: 'validator', type: principalT }
],
output: booleanT,
mode: 'readonly'
},
'is-whitelisted': {
input: [ { name: 'user', type: principalT } ],
output: booleanT,
mode: 'readonly'
},
'message-domain': { input: [], output: bufferT, mode: 'readonly' },
'user-from-id': {
input: [ { name: 'id', type: uintT } ],
output: optionalT(principalT, ),
mode: 'readonly'
},
'user-from-id-or-fail': {
input: [ { name: 'id', type: uintT } ],
output: responseSimpleT(principalT, ),
mode: 'readonly'
},
'validate-launchpad': {
input: [
{ name: 'launch-id', type: uintT },
{ name: 'from', type: bufferT },
{ name: 'to', type: principalT },
{ name: 'amount', type: uintT },
{ name: 'token', type: principalT }
],
output: responseSimpleT(tupleT({
'apower-to-burn': uintT,
offering: tupleT({
'activation-threshold': uintT,
'apower-per-ticket-in-fixed': listT(tupleT({ 'apower-per-ticket-in-fixed': uintT, 'tier-threshold': uintT }, ), ),
'claim-end-height': uintT,
'fee-per-ticket-in-fixed': uintT,
'launch-owner': principalT,
'launch-token': principalT,
'launch-tokens-per-ticket': uintT,
'max-size-factor': uintT,
'payment-token': principalT,
'price-per-ticket-in-fixed': uintT,
'registration-end-height': uintT,
'registration-max-tickets': uintT,
'registration-start-height': uintT,
'total-registration-max': uintT,
'total-tickets': uintT
}, ),
tickets: uintT
}, ), ),
mode: 'readonly'
},
'validator-from-id': {
input: [ { name: 'id', type: uintT } ],
output: optionalT(tupleT({ validator: principalT, 'validator-pubkey': bufferT }, ), ),
mode: 'readonly'
},
'validator-from-id-or-fail': {
input: [ { name: 'id', type: uintT } ],
output: responseSimpleT(tupleT({ validator: principalT, 'validator-pubkey': bufferT }, ), ),
mode: 'readonly'
},
'launch-whitelisted': {
input: tupleT({ 'launch-id': uintT, owner: bufferT }, ),
output: optionalT(booleanT, ),
mode: 'mapEntry'
},
'use-launch-whitelist': { input: uintT, output: optionalT(booleanT, ), mode: 'mapEntry' },
'whitelisted-users': {
input: principalT,
output: optionalT(booleanT, ),
mode: 'mapEntry'
},
'contract-owner': { input: noneT, output: principalT, mode: 'variable' },
'is-paused': { input: noneT, output: booleanT, mode: 'variable' },
'order-hash-to-iter': { input: noneT, output: bufferT, mode: 'variable' },
'use-whitelist': { input: noneT, output: booleanT, mode: 'variable' }
}
} as const)

View File

@@ -0,0 +1,10 @@
import { defineContract } from "../smartContractHelpers/codegenImport";
import { btcBridgeEndpointV111 } from "./contract_xlink_btc-bridge-endpoint-v1-11"
import { crossBridgeEndpointV103 } from "./contract_xlink_cross-bridge-endpoint-v1-03"
export const xlinkContracts = defineContract({
...btcBridgeEndpointV111,
...crossBridgeEndpointV103
});

View File

@@ -0,0 +1 @@
export * from "clarity-codegen"

61
package.json Normal file
View File

@@ -0,0 +1,61 @@
{
"name": "xlink-sdk",
"version": "0.0.1",
"description": "XLINK js SDK",
"keywords": [
"bitcoin",
"ethereum",
"stacks",
"XLINK",
"alexlab"
],
"repository": "github:alexgo-io/xlink-sdk",
"author": "c4605 <yuntao@alexgo.io>",
"license": "MIT",
"files": [
"lib",
"src",
"generated"
],
"main": "lib/index.js",
"exports": {
"./": "./lib/index.mjs"
},
"scripts": {
"gen:stacksContract": "rm -rf generated/smartContract && mkdir -p generated/smartContract && tsx ./scripts/generateClarityTranscoders.ts",
"gen": "pnpm run gen:stacksContract",
"build": "pnpm run gen && rm -rf lib && tsup-node --sourcemap --dts -d lib --format cjs,esm src",
"prepare": "pnpm run build",
"test": "vitest --exclude lib"
},
"dependencies": {
"@c4/btc-utils": "^0.2.0",
"big.js": "^6.2.1",
"clarity-codegen": "0.5.2",
"viem": "^2.9.16"
},
"devDependencies": {
"@c4605/toolconfs": "^5.3.0",
"@stacks/network": "^6.13.0",
"@stacks/stacks-blockchain-api-types": "^7.9.0",
"@stacks/transactions": "^6.13.0",
"@types/big.js": "^6.2.2",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"prettier": "^3.2.5",
"tsup": "^8.0.2",
"tsx": "^4.7.2",
"typescript": "^5.4.5",
"vitest": "^1.4.0"
},
"optionalDependencies": {
"@scure/btc-signer": "^1.2.2"
},
"peerDependencies": {
"@stacks/network": "^6.13.0",
"@stacks/transactions": "^6.13.0"
}
}

3136
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
import { generateContracts } from "clarity-codegen/lib/generate"
import * as path from "node:path"
import { STACKS_CONTRACT_DEPLOYER_MAINNET, STACKS_MAINNET } from "../src/config"
;(async function main(): Promise<void> {
await generateContracts(
STACKS_MAINNET.coreApiUrl,
STACKS_CONTRACT_DEPLOYER_MAINNET,
["btc-bridge-endpoint-v1-11", "cross-bridge-endpoint-v1-03"],
path.resolve(__dirname, "../generated/smartContract/"),
"xlink",
"../smartContractHelpers/codegenImport",
)
})().catch(console.error)

81
src/XLinkSDK.ts Normal file
View File

@@ -0,0 +1,81 @@
import { ChainId, SupportedToken } from "./xlinkSdkUtils/types"
import {
bridgeFromStacks,
supportedRoutes as supportedRoutesFromStacks,
} from "./xlinkSdkUtils/bridgeFromStacks"
import { ChainIdInternal, TokenIdInternal } from "./utils/types.internal"
import {
bridgeFromEthereum,
supportedRoutes as supportedRoutesFromEthereum,
} from "./xlinkSdkUtils/bridgeFromEthereum"
import {
bridgeFromBitcoin,
supportedRoutes as supportedRoutesFromBitcoin,
} from "./xlinkSdkUtils/bridgeFromBitcoin"
import { GetSupportedRoutesFnAnyResult } from "./utils/buildSupportedRoutes"
import { bridgeInfoFromBitcoin } from "./xlinkSdkUtils/bridgeInfoFromBitcoin"
import { bridgeInfoFromEthereum } from "./xlinkSdkUtils/bridgeInfoFromEthereum"
import { bridgeInfoFromStacks } from "./xlinkSdkUtils/bridgeInfoFromStacks"
export {
BridgeFromStacksInput,
BridgeFromStacksOutput,
} from "./xlinkSdkUtils/bridgeFromStacks"
export {
BridgeFromEthereumInput,
BridgeFromEthereumOutput,
} from "./xlinkSdkUtils/bridgeFromEthereum"
export {
BridgeFromBitcoinInput,
BridgeFromBitcoinOutput,
} from "./xlinkSdkUtils/bridgeFromBitcoin"
export {
BridgeInfoFromBitcoinInput,
BridgeInfoFromBitcoinOutput,
} from "./xlinkSdkUtils/bridgeInfoFromBitcoin"
export {
BridgeInfoFromEthereumInput,
BridgeInfoFromEthereumOutput,
} from "./xlinkSdkUtils/bridgeInfoFromEthereum"
export {
BridgeInfoFromStacksInput,
BridgeInfoFromStacksOutput,
} from "./xlinkSdkUtils/bridgeInfoFromStacks"
export class XLINKSDK {
async getSupportedTokens(
fromChain: ChainId,
toChain: ChainId,
): Promise<SupportedToken[]> {
for (const rules of [
supportedRoutesFromStacks,
supportedRoutesFromEthereum,
supportedRoutesFromBitcoin,
]) {
const result: GetSupportedRoutesFnAnyResult =
await rules.getSupportedTokens(fromChain, toChain)
if (result.length > 0) {
return result.flatMap(res => [
{
fromChain: ChainIdInternal.toChainId(res.fromChain),
fromToken: TokenIdInternal.toTokenId(res.fromToken),
toChain: ChainIdInternal.toChainId(res.toChain),
toToken: TokenIdInternal.toTokenId(res.toToken),
},
])
}
}
return []
}
bridgeInfoFromStacks = bridgeInfoFromStacks
bridgeFromStacks = bridgeFromStacks
bridgeInfoFromEthereum = bridgeInfoFromEthereum
bridgeFromEthereum = bridgeFromEthereum
bridgeInfoFromBitcoin = bridgeInfoFromBitcoin
bridgeFromBitcoin = bridgeFromBitcoin
}

View File

@@ -0,0 +1,50 @@
import type { EstimationInput } from "@c4/btc-utils"
import { sum } from "../utils/bigintHelpers"
import { Address, OutScript } from "@scure/btc-signer"
import { BigNumber } from "../utils/BigNumber"
export interface UTXOBasic {
txId: string
index: number
amount: bigint
}
export interface UTXOConfirmed extends UTXOBasic {
blockHeight: bigint
}
export type UTXOSpendable = EstimationInput &
UTXOBasic & {
scriptPubKey: Uint8Array
}
export interface BitcoinNetwork {
bech32: string
pubKeyHash: number
scriptHash: number
wif: number
}
export function sumUTXO(utxos: Array<UTXOBasic>): bigint {
return sum(utxos.map(utxo => utxo.amount))
}
export function isSameUTXO(utxo1: UTXOBasic, utxo2: UTXOBasic): boolean {
return utxo1.txId === utxo2.txId && utxo1.index === utxo2.index
}
export function addressToScriptPubKey(
network: BitcoinNetwork,
address: string,
): Uint8Array {
const addr = Address(network).decode(address)
return OutScript.encode(addr)
}
export function bitcoinToSatoshi(bitcoinAmount: string): bigint {
return BigNumber.toBigInt({}, BigNumber.rightMoveDecimals(8, bitcoinAmount))
}
export function satoshiToBitcoin(satoshiAmount: bigint): string {
return BigNumber.toString(BigNumber.leftMoveDecimals(8, satoshiAmount))
}

View File

@@ -0,0 +1,22 @@
import { encodeHex } from "../utils/hexHelpers"
import { mempoolFetch } from "./mempoolFetch"
export const broadcastSignedTransaction = async (
network: "mainnet" | "testnet",
transaction: string | Uint8Array,
): Promise<{ txId: string }> => {
const transactionHex =
typeof transaction === "string" ? transaction : encodeHex(transaction)
const res = await mempoolFetch<string>({
network,
method: "post",
path: "/tx",
body: transactionHex,
parseResponse: resp => resp.text(),
})
return {
txId: res,
}
}

View File

@@ -0,0 +1,28 @@
import { checkNever } from "../utils/typeHelpers"
import { KnownChainId } from "../utils/types.internal"
export interface BitcoinAddress {
address: string
}
export function getBTCPegInAddress(
network: KnownChainId.BitcoinChain,
): undefined | BitcoinAddress {
let addr: undefined | string
switch (network) {
case KnownChainId.Bitcoin.Mainnet:
addr = "bc1pylrcm2ym9spaszyrwzhhzc2qf8c3xq65jgmd8udqtd5q73a2fulsztxqyy"
break
case KnownChainId.Bitcoin.Testnet:
addr = "tb1qeprcndv9n8luumegjsnljjcf68e7ay62n5a667"
break
default:
checkNever(network)
}
if (addr == null) return undefined
return {
address: addr,
}
}

View File

@@ -0,0 +1,54 @@
import * as btc from "@scure/btc-signer"
import { BitcoinNetwork, UTXOBasic } from "./bitcoinHelpers"
import { Recipient, createTransaction } from "./createTransaction"
import { prepareTransaction } from "./prepareTransaction"
import {
GetConfirmedSpendableUTXOFn,
reselectSpendableUTXOsFactory,
} from "./selectUTXOs"
export async function createSendBitcoinTransaction(options: {
network: BitcoinNetwork
recipients: Recipient[]
changeAddress: string
opReturnData?: Uint8Array[]
feeRate: bigint
availableFeeUtxos: UTXOBasic[]
getUTXOSpendable: GetConfirmedSpendableUTXOFn
}): Promise<{
tx: btc.Transaction
}> {
const opReturnData = options.opReturnData ?? []
const {
inputs,
recipients: newRecipients,
changeAmount,
} = await prepareTransaction({
recipients: options.recipients,
changeAddress: options.changeAddress,
feeRate: options.feeRate,
opReturnData,
network: options.network,
reselectSpendableUTXOs: reselectSpendableUTXOsFactory(
options.availableFeeUtxos,
options.getUTXOSpendable,
),
})
const tx = createTransaction(
options.network,
inputs,
newRecipients.concat(
changeAmount === 0n
? []
: {
address: options.changeAddress,
satsAmount: changeAmount,
},
),
opReturnData,
)
return { tx }
}

View File

@@ -0,0 +1,85 @@
import * as btc from "@scure/btc-signer"
import { BitcoinNetwork, isSameUTXO, UTXOBasic } from "./bitcoinHelpers"
import { isNotNull } from "../utils/typeHelpers"
import { createTransaction } from "./createTransaction"
import { prepareTransaction } from "./prepareTransaction"
import {
GetConfirmedSpendableUTXOFn,
reselectSpendableUTXOsFactory,
} from "./selectUTXOs"
export interface InscriptionRecipient {
inscriptionUtxo: UTXOBasic
address: string
}
export async function createSendInscriptionTransaction(options: {
network: BitcoinNetwork
inscriptionRecipients: InscriptionRecipient[]
changeAddress: string
opReturnData?: Uint8Array[]
availableFeeUtxos: UTXOBasic[]
feeRate: bigint
getUTXOSpendable: GetConfirmedSpendableUTXOFn
}): Promise<{
tx: btc.Transaction
inscriptionUtxoInputIndices: number[]
bitcoinUtxoInputIndices: number[]
}> {
const opReturnData = options.opReturnData ?? []
const recipients = options.inscriptionRecipients.map(r => ({
address: r.address,
satsAmount: r.inscriptionUtxo.amount,
}))
const selectedUTXOs = await Promise.all(
options.inscriptionRecipients.map(r =>
options.getUTXOSpendable(r.inscriptionUtxo),
),
).then(utxos => utxos.filter(isNotNull))
const {
inputs,
recipients: newRecipients,
changeAmount,
} = await prepareTransaction({
network: options.network,
recipients,
changeAddress: options.changeAddress,
opReturnData,
feeRate: options.feeRate,
selectedUTXOs,
reselectSpendableUTXOs: reselectSpendableUTXOsFactory(
options.availableFeeUtxos.filter(
u => !selectedUTXOs.find(_u => isSameUTXO(u, _u)),
),
options.getUTXOSpendable,
),
})
const tx = createTransaction(
options.network,
inputs,
newRecipients.concat(
changeAmount === 0n
? []
: {
address: options.changeAddress,
satsAmount: changeAmount,
},
),
opReturnData,
)
const inscriptionCount = options.inscriptionRecipients.length
return {
tx,
inscriptionUtxoInputIndices: inputs
.slice(0, inscriptionCount)
.map((_, i) => i),
bitcoinUtxoInputIndices: inputs
.slice(inscriptionCount)
.map((_, i) => i + inscriptionCount),
}
}

View File

@@ -0,0 +1,54 @@
import * as btc from "@scure/btc-signer"
import { hasAny } from "../utils/arrayHelpers"
import { BitcoinNetwork, UTXOSpendable } from "./bitcoinHelpers"
export interface Recipient {
address: string
satsAmount: bigint
}
export function createTransaction(
network: BitcoinNetwork,
inputUTXOs: Array<UTXOSpendable>,
recipients: Array<Recipient>,
opReturnData: Uint8Array[],
): btc.Transaction {
const tx = new btc.Transaction({
allowUnknownOutputs: true,
allowLegacyWitnessUtxo: true,
})
inputUTXOs.forEach(utxo => {
tx.addInput({
txid: utxo.txId,
index: utxo.index,
witnessUtxo:
utxo.scriptPubKey == null
? undefined
: {
script: utxo.scriptPubKey,
amount: utxo.amount,
},
tapInternalKey:
"tapInternalKey" in utxo ? utxo.tapInternalKey : undefined,
redeemScript: "redeemScript" in utxo ? utxo.redeemScript : undefined,
// Enable RBF
sequence: btc.DEFAULT_SEQUENCE - 2,
})
})
recipients.forEach(recipient => {
tx.addOutputAddress(recipient.address, recipient.satsAmount, network)
})
if (hasAny(opReturnData)) {
opReturnData.forEach(data => {
tx.addOutput({
script: btc.Script.encode(["RETURN", data]),
amount: 0n,
})
})
}
return tx
}

View File

@@ -0,0 +1,21 @@
export class InsufficientBalanceError extends Error {
constructor(...args: ConstructorParameters<typeof Error>) {
super(...args)
this.message =
this.message || "Insufficient Bitcoin balance with network fees"
}
}
export class UnsupportedBitcoinInput extends Error {
constructor(
txid: string,
index: number,
...args: ConstructorParameters<typeof Error>
) {
super(...args)
this.message =
this.message || `Not supported bitcoin input: ${txid}:${index}`
}
}

View File

@@ -0,0 +1,83 @@
import { XLINKSDKErrorBase } from "../utils/errors"
import { isPlainObject } from "../utils/isPlainObject"
let previousPromise: undefined | Promise<Response>
export const mempoolFetch = async <T>(options: {
network: "mainnet" | "testnet"
method?: "get" | "post"
path: string
headers?: Record<string, string | number>
query?: Record<string, string | number>
body?: any
parseResponse?: (resp: Response) => Promise<T>
avoidCache?: boolean
}): Promise<T> => {
const {
method: opMethod = "get",
query: opQuery = {},
body: opBody,
parseResponse: opParseResponse = resp => resp.json(),
} = options
const queryPairs = Object.entries({
...(options.avoidCache ? { ___: Date.now() } : {}),
...opQuery,
})
const querystring = new URLSearchParams(queryPairs as string[][]).toString()
const headers = new Headers(options.headers as Record<string, string>)
if (
opMethod === "post" &&
isPlainObject(opBody) &&
!headers.has("content-type")
) {
headers.set("content-type", "application/json")
}
const requestUrl =
"https://" +
getMempoolAPIPrefix(options.network) +
options.path.replace(/^\//, "") +
"?" +
querystring
previousPromise = (previousPromise ?? Promise.resolve()).then(() =>
fetch(requestUrl, {
method: opMethod,
headers: headers,
body:
headers.get("content-type")?.toLowerCase() === "application/json"
? JSON.stringify(opBody)
: opBody,
}),
)
const resp = await previousPromise
if (resp.status < 200 || resp.status >= 300) {
return resp.text().then(respText => {
throw new MempoolRequestError(respText, resp)
})
}
return opParseResponse(resp)
}
const getMempoolAPIPrefix = (network: "mainnet" | "testnet"): string => {
return `mempool.space${network === "mainnet" ? "" : `/${network}`}/api/`
}
export class MempoolRequestError extends XLINKSDKErrorBase {
cause: {
data: any
resp: Response
}
constructor(data: any, resp: Response) {
super("Request mempool.space failed: " + JSON.stringify(data))
this.cause = {
data,
resp,
}
}
}

View File

@@ -0,0 +1,196 @@
import {
estimateTransactionVSizeAfterSign,
EstimationOutput,
getOutputDustThreshold,
UnsupportedInputTypeError,
} from "@c4/btc-utils"
import * as btc from "@scure/btc-signer"
import { InsufficientBalanceError, UnsupportedBitcoinInput } from "./errors"
import { max, sum } from "../utils/bigintHelpers"
import {
addressToScriptPubKey,
BitcoinNetwork,
sumUTXO,
UTXOSpendable,
} from "./bitcoinHelpers"
import { Recipient as _Recipient } from "./createTransaction"
export type Recipient = _Recipient
export type ReselectSpendableUTXOsFn = (
satsToSend: bigint,
pinnedUTXOs: UTXOSpendable[],
lastTimeSelectedUTXOs: UTXOSpendable[],
) => Promise<UTXOSpendable[]>
export async function prepareTransaction(txInfo: {
network: BitcoinNetwork
recipients: Array<Recipient>
changeAddress: string
opReturnData?: Uint8Array[]
selectedUTXOs?: Array<UTXOSpendable>
feeRate: bigint
reselectSpendableUTXOs: ReselectSpendableUTXOsFn
}): Promise<{
inputs: Array<UTXOSpendable>
recipients: Array<Recipient>
changeAmount: bigint
fee: bigint
}> {
const {
network,
recipients,
changeAddress,
opReturnData = [],
selectedUTXOs = [],
feeRate,
reselectSpendableUTXOs,
} = txInfo
const newRecipients = await Promise.all(
recipients.map(async (r): Promise<_Recipient> => {
const dustThreshold = await getOutputDustThresholdForOutput(
network,
r.address,
)
return {
...r,
satsAmount: max([r.satsAmount, dustThreshold]),
}
}),
)
const newRecipientAddresses = newRecipients
.map(r => r.address)
.concat(changeAddress)
const satsToSend = sum(newRecipients.map(r => r.satsAmount))
let lastSelectedUTXOs = selectedUTXOs.slice()
let lastSelectedUTXOSatsInTotal = sumUTXO(lastSelectedUTXOs)
// Calculate fee
let calculatedFee = await calculateFee(
network,
newRecipientAddresses,
opReturnData,
lastSelectedUTXOs,
feeRate,
)
let loopTimes = 0
while (lastSelectedUTXOSatsInTotal < satsToSend + calculatedFee) {
const newSatsToSend = satsToSend + calculatedFee
const newSelectedUTXOs = await reselectSpendableUTXOs(
newSatsToSend,
selectedUTXOs,
lastSelectedUTXOs,
)
const newSelectedUTXOSatsInTotal = sumUTXO(newSelectedUTXOs)
// Check if selected UTXO satoshi amount has changed since last iteration
// If it hasn't, there is insufficient balance
if (newSelectedUTXOSatsInTotal < lastSelectedUTXOSatsInTotal) {
throw new InsufficientBalanceError()
}
lastSelectedUTXOSatsInTotal = newSelectedUTXOSatsInTotal
lastSelectedUTXOs = newSelectedUTXOs
// Re-calculate fee
calculatedFee = await calculateFee(
network,
newRecipientAddresses,
opReturnData,
lastSelectedUTXOs,
feeRate,
)
loopTimes++
if (loopTimes > 500) {
// Exit after max 500 iterations
throw new InsufficientBalanceError()
}
}
const changeOutputDustThreshold = await getOutputDustThresholdForOutput(
network,
changeAddress,
)
const changeAmount =
lastSelectedUTXOSatsInTotal - sum([satsToSend, calculatedFee])
let finalChangeAmount: bigint
let finalFeeAmount: bigint
if (changeAmount < changeOutputDustThreshold) {
finalChangeAmount = 0n
finalFeeAmount = lastSelectedUTXOSatsInTotal - satsToSend
} else {
finalChangeAmount = changeAmount
finalFeeAmount = calculatedFee
}
return {
inputs: lastSelectedUTXOs,
recipients: newRecipients,
changeAmount: finalChangeAmount,
fee: finalFeeAmount,
}
}
async function getOutputDustThresholdForOutput(
network: BitcoinNetwork,
outputAddress: string,
): Promise<bigint> {
return BigInt(
Math.ceil(
getOutputDustThreshold({
scriptPubKey: addressToScriptPubKey(network, outputAddress),
}),
),
)
}
/**
* https://github.com/bitcoin-dot-org/developer.bitcoin.org/blob/813ba3fb5eae85cfdfffe91d12f2df653ea8b725/devguide/transactions.rst?plain=1#L314
* https://github.com/bitcoin/bitcoin/blob/2ffaa927023f5dc2a7b8d6cfeb4f4810e573b18c/src/policy/policy.h#L57
*/
const DEFAULT_MIN_RELAY_TX_FEE = 1000n
async function calculateFee(
network: BitcoinNetwork,
recipientAddresses: string[],
opReturnData: Uint8Array[],
selectedUTXOs: Array<UTXOSpendable>,
feeRate: bigint,
): Promise<bigint> {
const outputs: EstimationOutput[] = [
...recipientAddresses.map(r => ({
scriptPubKey: addressToScriptPubKey(network, r),
})),
...opReturnData.map(data => ({
scriptPubKey: btc.Script.encode(["RETURN", data]),
})),
]
try {
const txSize = BigInt(
Math.ceil(
estimateTransactionVSizeAfterSign({
inputs: selectedUTXOs,
outputs,
}),
),
)
return max([feeRate * txSize, DEFAULT_MIN_RELAY_TX_FEE])
} catch (e) {
if (e instanceof UnsupportedInputTypeError) {
const input = e.cause as UTXOSpendable
throw new UnsupportedBitcoinInput(input.txId, input.index)
}
throw e
}
}

View File

@@ -0,0 +1,116 @@
import { hasAny, sortBy } from "../utils/arrayHelpers"
import { MAX_BIGINT, sum } from "../utils/bigintHelpers"
import {
isSameUTXO,
sumUTXO,
UTXOBasic,
UTXOConfirmed,
UTXOSpendable,
} from "./bitcoinHelpers"
import { decodeHex } from "../utils/hexHelpers"
import { isNotNull } from "../utils/typeHelpers"
import { ReselectSpendableUTXOsFn } from "./prepareTransaction"
export type GetConfirmedSpendableUTXOFn = (
utxo: UTXOBasic,
) => Promise<undefined | (UTXOSpendable & UTXOConfirmed)>
export const reselectSpendableUTXOsFactory = (
availableUTXOs: UTXOBasic[],
getUTXOSpendable: GetConfirmedSpendableUTXOFn,
): ReselectSpendableUTXOsFn => {
return async (satsToSend, pinnedUTXOs, _lastTimeSelectedUTXOs) => {
const lastTimeSelectedUTXOs = await Promise.all(
_lastTimeSelectedUTXOs.map(getUTXOSpendable),
).then(utxos => utxos.filter(isNotNull))
const otherAvailableUTXOs = await Promise.all(
availableUTXOs
.filter(
availableUTXO =>
!lastTimeSelectedUTXOs.find(selectedUTXO =>
isSameUTXO(availableUTXO, selectedUTXO),
),
)
.map(getUTXOSpendable),
).then(utxos => utxos.filter(isNotNull))
return selectUTXOs(satsToSend, lastTimeSelectedUTXOs, otherAvailableUTXOs)
}
}
export const reselectSpendableUTXOsWithSafePadFactory = (
availableUTXOs: UTXOBasic[],
getUTXOSpendable: GetConfirmedSpendableUTXOFn,
): ReselectSpendableUTXOsFn => {
const reselect = reselectSpendableUTXOsFactory(
availableUTXOs,
getUTXOSpendable,
)
return async (satsToSend, pinnedUTXOs, lastTimeSelectedUTXOs) => {
const utxos = await reselect(satsToSend, pinnedUTXOs, lastTimeSelectedUTXOs)
const selectedAmount = sumUTXO(utxos)
const difference = satsToSend - selectedAmount
if (difference > 0n) {
return utxos.concat({
addressType: "p2pkh",
txId: "0000000000000000000000000000000000000000000000000000000000000000",
index: 0,
amount: MAX_BIGINT,
/**
* OutScript.bencode({
* type: 'pkh',
* hash: hash160(secp256k1.getPublicKey(
* hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
* false,
* ))
* })
*/
scriptPubKey: decodeHex(
"76a91491b24bf9f5288532960ac687abb035127b1d28a588ac",
),
isPublicKeyCompressed: false,
})
}
return utxos
}
}
export function selectUTXOs<T extends UTXOConfirmed>(
satsToSend: bigint,
selectedUTXOs: T[],
otherAvailableUTXOs: T[],
): T[] {
const inputs: Array<T> = []
let sumValue = 0n
otherAvailableUTXOs = otherAvailableUTXOs.slice()
if (hasAny(selectedUTXOs)) {
inputs.push(...selectedUTXOs)
sumValue = sum([sumValue, ...selectedUTXOs.map(o => o.amount)])
}
// Sort UTXOs:
// 1. By amount in descending order
// 2. By block height in ascending order
otherAvailableUTXOs = sortBy(
[o => -o.amount, o => o.blockHeight],
otherAvailableUTXOs,
)
for (const utxo of otherAvailableUTXOs) {
inputs.push(utxo)
sumValue = sumValue + utxo.amount
if (sumValue >= satsToSend) {
break
}
}
return inputs
}

32
src/config.ts Normal file
View File

@@ -0,0 +1,32 @@
import { StacksMainnet, StacksMocknet } from "@stacks/network"
import { createClient, http } from "viem"
import { bsc, bscTestnet, mainnet, sepolia } from "viem/chains"
export const STACKS_CONTRACT_DEPLOYER_MAINNET =
"SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9"
export const STACKS_CONTRACT_DEPLOYER_TESTNET =
"ST1J2JTYXGRMZYNKE40GM87ZCACSPSSEEQVSNB7DC"
export const STACKS_MAINNET = new StacksMainnet({
url: "https://stacks-node-api.alexlab.co",
})
export const STACKS_TESTNET = new StacksMocknet({
url: "https://stacks-node-api.alexgo.dev",
})
export const ETHEREUM_MAINNET_CLIENT = createClient({
chain: mainnet,
transport: http(),
})
export const ETHEREUM_SEPOLIA_CLIENT = createClient({
chain: sepolia,
transport: http(),
})
export const ETHEREUM_BSC_CLIENT = createClient({
chain: bsc,
transport: http(),
})
export const ETHEREUM_BSCTESTNET_CLIENT = createClient({
chain: bscTestnet,
transport: http(),
})

View File

@@ -0,0 +1,142 @@
export const bridgeEndpointAbi = [
{
inputs: [
{
internalType: "address",
name: "token",
type: "address",
},
{
internalType: "uint256",
name: "amount",
type: "uint256",
},
{
internalType: "string",
name: "settleData",
type: "string",
},
{
internalType: "uint256",
name: "launchId",
type: "uint256",
},
],
name: "transferToLaunchpad",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "token",
type: "address",
},
{
internalType: "uint256",
name: "amount",
type: "uint256",
},
{
internalType: "string",
name: "settleData",
type: "string",
},
],
name: "transferToWrap",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [],
name: "paused",
outputs: [
{
internalType: "bool",
name: "",
type: "bool",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "",
type: "address",
},
],
name: "feePctPerToken",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "",
type: "address",
},
],
name: "minFeePerToken",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "",
type: "address",
},
],
name: "minAmountPerToken",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "",
type: "address",
},
],
name: "maxAmountPerToken",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
] as const

View File

@@ -0,0 +1,19 @@
import { UnsupportedChainError } from "../utils/errors"
import { checkNever } from "../utils/typeHelpers"
import { KnownChainId } from "../utils/types.internal"
export function contractAssignedChainIdFromBridgeChain(
chain: KnownChainId.EthereumChain,
): bigint {
switch (chain) {
case KnownChainId.Ethereum.Mainnet:
case KnownChainId.Ethereum.Sepolia:
return 1n
case KnownChainId.Ethereum.BSC:
case KnownChainId.Ethereum.BSCTest:
return 2n
default:
checkNever(chain)
throw new UnsupportedChainError(chain)
}
}

View File

@@ -0,0 +1,86 @@
import { Address } from "viem"
import { KnownChainId, KnownTokenId } from "../utils/types.internal"
type ETHChain = KnownChainId.EthereumChain
const ETHChain = KnownChainId.Ethereum
const ETHToken = KnownTokenId.Ethereum
type AddressMap = Record<ETHChain, undefined | Address>
export type EndpointContractAddresses = Record<
keyof typeof ethEndpointContractAddresses,
AddressMap
>
export type TokenContractAddresses = Record<
keyof typeof ethTokenContractAddresses,
AddressMap
>
export const ethEndpointContractAddresses = {
bridgeEndpoint: {
[ETHChain.Mainnet]: "0xfd9F795B4C15183BDbA83dA08Da02D5f9536748f",
[ETHChain.Sepolia]: "0x84a0cc1ab353dA6b7817947F7B116b8ea982C3D2",
[ETHChain.BSC]: "0xb3955302E58FFFdf2da247E999Cd9755f652b13b",
[ETHChain.BSCTest]: "0xF67734B5b137E26Df05C6Dd4B12f1bC65a0A53E7",
},
} as const
export const ethTokenContractAddresses: Record<string, AddressMap> = {
// [ETHCurrency.USDC]: {
// [ETHChain.Ethereum]: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
// [ETHChain.Goerli]: "0x7Ffd58D5bB024A982D918B127F9AbEf2C974dFCD",
// [ETHChain.AVAX]: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
// [ETHChain.BSC]: "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d",
// [ETHChain.Polygon]: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
// },
[ETHToken.USDT]: {
[ETHChain.Mainnet]: "0xdac17f958d2ee523a2206206994597c13d831ec7",
[ETHChain.Sepolia]: "0xBa175fDaB00e7FCF603f43bE8f68dB7f4de9f3A9",
[ETHChain.BSC]: "0x55d398326f99059ff775485246999027b3197955",
[ETHChain.BSCTest]: undefined,
// [ETHChain.Polygon]: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",
// [ETHChain.AVAX]: "0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7",
},
[ETHToken.LUNR]: {
[ETHChain.Mainnet]: "0xA87135285Ae208e22068AcDBFf64B11Ec73EAa5A",
[ETHChain.Sepolia]: "0x50c99C14eD859Cde37f6badE0b3887B30D028386",
[ETHChain.BSC]: "0x37807D4fbEB84124347B8899Dd99616090D3e304",
[ETHChain.BSCTest]: undefined,
// [ETHChain.Polygon]: undefined,
// [ETHChain.AVAX]: undefined,
},
[ETHToken.WBTC]: {
[ETHChain.Mainnet]: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
[ETHChain.Sepolia]: "0x5aCb7fC4b3Bbc875bEd4ebAB6CeDD79DCa17C035",
[ETHChain.BSC]: undefined,
[ETHChain.BSCTest]: undefined,
// [ETHChain.Polygon]: "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6",
// [ETHChain.AVAX]: "0x50b7545627a5162F82A992c33b87aDc75187B218",
},
[ETHToken.BTCB]: {
[ETHChain.Mainnet]: undefined,
[ETHChain.Sepolia]: undefined,
[ETHChain.BSC]: "0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c",
[ETHChain.BSCTest]: undefined,
// [ETHChain.Polygon]: undefined,
// [ETHChain.AVAX]: undefined,
},
[ETHToken.ALEX]: {
[ETHChain.Mainnet]: "0xe7c3755482d0da522678af05945062d4427e0923",
[ETHChain.Sepolia]: "0x9369e86F99613c801D9cf7082f87B2794DAbA1C4",
[ETHChain.BSC]: "0x43781e3533fa9b8a84823559a22d171825599b8f",
[ETHChain.BSCTest]: undefined,
// [ETHChain.Polygon]: undefined,
// [ETHChain.AVAX]: undefined,
},
[ETHToken.SKO]: {
[ETHChain.Mainnet]: undefined,
[ETHChain.Sepolia]: undefined,
[ETHChain.BSC]: "0x9Bf543D8460583Ff8a669Aae01d9cDbeE4dEfE3c",
[ETHChain.BSCTest]: undefined,
// [ETHChain.Polygon]: undefined,
// [ETHChain.AVAX]: undefined,
},
}

View File

@@ -0,0 +1,89 @@
import { Address, Client } from "viem"
import {
ETHEREUM_BSCTESTNET_CLIENT,
ETHEREUM_BSC_CLIENT,
ETHEREUM_MAINNET_CLIENT,
ETHEREUM_SEPOLIA_CLIENT,
} from "../config"
import { BigNumber, BigNumberSource } from "../utils/BigNumber"
import { checkNever } from "../utils/typeHelpers"
import { KnownChainId, TokenIdInternal } from "../utils/types.internal"
import { ethTokenContractAddresses } from "./ethContractAddresses"
const CONTRACT_COMMON_NUMBER_SCALE = 18
export const numberFromSolidityContractNumber = (
num: bigint,
decimals?: number,
): BigNumber => {
return BigNumber.leftMoveDecimals(
decimals ?? CONTRACT_COMMON_NUMBER_SCALE,
num,
)
}
export const numberToSolidityContractNumber = (
num: BigNumberSource,
decimals?: number,
): bigint => {
return BigNumber.toBigInt(
{},
BigNumber.rightMoveDecimals(decimals ?? CONTRACT_COMMON_NUMBER_SCALE, num),
)
}
export const getContractCallInfo = (
chainId: KnownChainId.EthereumChain,
):
| undefined
| {
client: Client
} => {
if (chainId === KnownChainId.Ethereum.Mainnet) {
return {
client: ETHEREUM_MAINNET_CLIENT,
}
}
if (chainId === KnownChainId.Ethereum.BSC) {
return {
client: ETHEREUM_BSC_CLIENT,
}
}
if (chainId === KnownChainId.Ethereum.Sepolia) {
return {
client: ETHEREUM_SEPOLIA_CLIENT,
}
}
if (chainId === KnownChainId.Ethereum.BSCTest) {
return {
client: ETHEREUM_BSCTESTNET_CLIENT,
}
}
checkNever(chainId)
return
}
export const getTokenContractInfo = (
chainId: KnownChainId.EthereumChain,
tokenId: TokenIdInternal,
):
| undefined
| {
client: Client
contractAddress: Address
} => {
const contractCallInfo = getContractCallInfo(chainId)
if (
contractCallInfo == null ||
ethTokenContractAddresses[tokenId]?.[chainId] == null
) {
return undefined
}
const { client } = contractCallInfo
return {
client,
contractAddress: ethTokenContractAddresses[tokenId]![chainId]!,
}
}

2
src/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./xlinkSdkUtils/types"
export * from "./XLINKSDK"

View File

@@ -0,0 +1,91 @@
import { CallReadOnlyFunctionFn, unwrapResponse } from "clarity-codegen"
import { checkNever } from "../utils/typeHelpers"
import { executeReadonlyCallXLINK } from "./xlinkContractHelpers"
import { StacksNetwork } from "@stacks/network"
import { callReadOnlyFunction } from "@stacks/transactions"
export interface BridgeSwapRouteNode {
poolId: bigint
tokenContractAddress: `${string}.${string}::${string}`
}
export type BridgeSwapRoute_BitcoinToStacks =
| []
| [BridgeSwapRouteNode]
| [BridgeSwapRouteNode, BridgeSwapRouteNode]
| [BridgeSwapRouteNode, BridgeSwapRouteNode, BridgeSwapRouteNode]
export async function createBridgeOrder_BitcoinToStack(info: {
endpointDeployerAddress: string
network: StacksNetwork
receiverStxAddr: string
swapSlippedAmount: bigint
swapRoute: BridgeSwapRoute_BitcoinToStacks
}): Promise<{ data: Uint8Array }> {
let data: undefined | Uint8Array
const { swapRoute, receiverStxAddr, swapSlippedAmount } = info
const executeOptions = {
deployerAddress: info.endpointDeployerAddress,
callReadOnlyFunction: (callOptions =>
callReadOnlyFunction({
...callOptions,
network: info.network,
})) satisfies CallReadOnlyFunctionFn,
}
if (swapRoute.length === 0) {
data = await executeReadonlyCallXLINK(
"btc-bridge-endpoint-v1-11",
"create-order-0-or-fail",
{ order: receiverStxAddr },
executeOptions,
).then(unwrapResponse)
} else if (swapRoute.length === 1) {
data = await executeReadonlyCallXLINK(
"btc-bridge-endpoint-v1-11",
"create-order-1-or-fail",
{
order: {
"pool-id": swapRoute[0].poolId,
"min-dy": swapSlippedAmount,
user: receiverStxAddr,
},
},
executeOptions,
).then(unwrapResponse)
} else if (swapRoute.length === 2) {
data = await executeReadonlyCallXLINK(
"btc-bridge-endpoint-v1-11",
"create-order-2-or-fail",
{
order: {
"pool1-id": swapRoute[0].poolId,
"pool2-id": swapRoute[1].poolId,
"min-dz": swapSlippedAmount,
user: receiverStxAddr,
},
},
executeOptions,
).then(unwrapResponse)
} else if (swapRoute.length === 3) {
data = await executeReadonlyCallXLINK(
"btc-bridge-endpoint-v1-11",
"create-order-3-or-fail",
{
order: {
"pool1-id": swapRoute[0].poolId,
"pool2-id": swapRoute[1].poolId,
"pool3-id": swapRoute[2].poolId,
"min-dw": swapSlippedAmount,
user: receiverStxAddr,
},
},
executeOptions,
).then(unwrapResponse)
} else {
checkNever(swapRoute)
}
return { data: data! }
}

View File

@@ -0,0 +1,65 @@
import {
STACKS_CONTRACT_DEPLOYER_MAINNET,
STACKS_CONTRACT_DEPLOYER_TESTNET,
} from "../config"
import { KnownChainId, KnownTokenId } from "../utils/types.internal"
interface ContractInfo {
contractAddress: string
contractName: string
}
export const stxTokenContractAddresses: Partial<
Record<string, Record<KnownChainId.StacksChain, ContractInfo>>
> = {
[KnownTokenId.Stacks.ALEX]: {
[KnownChainId.Stacks.Mainnet]: {
contractAddress: STACKS_CONTRACT_DEPLOYER_MAINNET,
contractName: "age000-governance-token",
},
[KnownChainId.Stacks.Testnet]: {
contractAddress: STACKS_CONTRACT_DEPLOYER_TESTNET,
contractName: "age000-governance-token",
},
},
[KnownTokenId.Stacks.aBTC]: {
[KnownChainId.Stacks.Mainnet]: {
contractAddress: STACKS_CONTRACT_DEPLOYER_MAINNET,
contractName: "token-abtc",
},
[KnownChainId.Stacks.Testnet]: {
contractAddress: STACKS_CONTRACT_DEPLOYER_TESTNET,
contractName: "token-abtc",
},
},
[KnownTokenId.Stacks.sUSDT]: {
[KnownChainId.Stacks.Mainnet]: {
contractAddress: STACKS_CONTRACT_DEPLOYER_MAINNET,
contractName: "token-susdt",
},
[KnownChainId.Stacks.Testnet]: {
contractAddress: STACKS_CONTRACT_DEPLOYER_TESTNET,
contractName: "token-susdt",
},
},
[KnownTokenId.Stacks.sLUNR]: {
[KnownChainId.Stacks.Mainnet]: {
contractAddress: STACKS_CONTRACT_DEPLOYER_MAINNET,
contractName: "token-slunr",
},
[KnownChainId.Stacks.Testnet]: {
contractAddress: STACKS_CONTRACT_DEPLOYER_TESTNET,
contractName: "token-slunr",
},
},
[KnownTokenId.Stacks.sSKO]: {
[KnownChainId.Stacks.Mainnet]: {
contractAddress: STACKS_CONTRACT_DEPLOYER_MAINNET,
contractName: "token-ssko",
},
[KnownChainId.Stacks.Testnet]: {
contractAddress: STACKS_CONTRACT_DEPLOYER_TESTNET,
contractName: "token-ssko",
},
},
}

View File

@@ -0,0 +1,86 @@
import { CallReadOnlyFunctionFn, Response } from "clarity-codegen"
import { checkNever } from "../utils/typeHelpers"
import { executeReadonlyCallXLINK } from "./xlinkContractHelpers"
import { BridgeSwapRoute_BitcoinToStacks } from "./createBridgeOrder"
import { callReadOnlyFunction } from "@stacks/transactions"
import { StacksNetwork } from "@stacks/network"
export async function validateBridgeOrder_BitcoinToStack(info: {
endpointDeployerAddress: string
network: StacksNetwork
btcTx: Uint8Array
swapRoute: BridgeSwapRoute_BitcoinToStacks
}): Promise<void> {
const { btcTx, swapRoute } = info
const executeOptions = {
deployerAddress: info.endpointDeployerAddress,
callReadOnlyFunction: (callOptions =>
callReadOnlyFunction({
...callOptions,
network: info.network,
})) satisfies CallReadOnlyFunctionFn,
}
let resp: Response<any>
if (swapRoute.length === 0) {
resp = await executeReadonlyCallXLINK(
"btc-bridge-endpoint-v1-11",
"validate-tx-0",
{
tx: btcTx,
"output-idx": 0n,
"order-idx": 2n,
},
executeOptions,
)
} else if (swapRoute.length === 1) {
resp = await executeReadonlyCallXLINK(
"btc-bridge-endpoint-v1-11",
"validate-tx-1",
{
tx: btcTx,
"output-idx": 0n,
"order-idx": 2n,
token: swapRoute[0].tokenContractAddress,
},
executeOptions,
)
} else if (swapRoute.length === 2) {
resp = await executeReadonlyCallXLINK(
"btc-bridge-endpoint-v1-11",
"validate-tx-2",
{
tx: btcTx,
"output-idx": 0n,
"order-idx": 2n,
token1: swapRoute[0].tokenContractAddress,
token2: swapRoute[1].tokenContractAddress,
},
executeOptions,
)
} else if (swapRoute.length === 3) {
resp = await executeReadonlyCallXLINK(
"btc-bridge-endpoint-v1-11",
"validate-tx-3",
{
tx: btcTx,
"output-idx": 0n,
"order-idx": 2n,
token1: swapRoute[0].tokenContractAddress,
token2: swapRoute[1].tokenContractAddress,
token3: swapRoute[2].tokenContractAddress,
},
executeOptions,
)
} else {
checkNever(swapRoute)
throw new Error(
`[validateBridgeOrder_BitcoinToStack] unsupported swap route length: ${(swapRoute as any).length}`,
)
}
if (resp.type === "success") return
throw resp.error
}

View File

@@ -0,0 +1,95 @@
import {
composeTxOptionsFactory,
executeReadonlyCallFactory,
} from "clarity-codegen"
import { xlinkContracts } from "../../generated/smartContract/contracts_xlink"
import { KnownChainId, TokenIdInternal } from "../utils/types.internal"
import {
STACKS_CONTRACT_DEPLOYER_MAINNET,
STACKS_CONTRACT_DEPLOYER_TESTNET,
STACKS_MAINNET,
STACKS_TESTNET,
} from "../config"
import { StacksNetwork } from "@stacks/network"
import { stxTokenContractAddresses } from "./stxContractAddresses"
import { BigNumber, BigNumberSource } from "../utils/BigNumber"
import { checkNever } from "../utils/typeHelpers"
const CONTRACT_COMMON_NUMBER_SCALE = 8
export const numberFromStacksContractNumber = (
num: bigint,
decimals?: number,
): BigNumber => {
return BigNumber.leftMoveDecimals(
decimals ?? CONTRACT_COMMON_NUMBER_SCALE,
num,
)
}
export const numberToStacksContractNumber = (
num: BigNumberSource,
decimals?: number,
): bigint => {
return BigNumber.toBigInt(
{},
BigNumber.rightMoveDecimals(decimals ?? CONTRACT_COMMON_NUMBER_SCALE, num),
)
}
export const composeTxXLINK = composeTxOptionsFactory(xlinkContracts, {})
export const executeReadonlyCallXLINK = executeReadonlyCallFactory(
xlinkContracts,
{},
)
export const getContractCallInfo = (
chainId: KnownChainId.StacksChain,
):
| undefined
| {
network: StacksNetwork
deployerAddress: string
} => {
if (chainId === KnownChainId.Stacks.Mainnet) {
return {
deployerAddress: STACKS_CONTRACT_DEPLOYER_MAINNET,
network: STACKS_MAINNET,
}
}
if (chainId === KnownChainId.Stacks.Testnet) {
return {
deployerAddress: STACKS_CONTRACT_DEPLOYER_TESTNET,
network: STACKS_TESTNET,
}
}
checkNever(chainId)
return
}
export const getTokenContractInfo = (
chainId: KnownChainId.StacksChain,
tokenId: TokenIdInternal,
):
| undefined
| {
network: StacksNetwork
deployerAddress: string
contractName: string
} => {
const contractCallInfo = getContractCallInfo(chainId)
if (
contractCallInfo == null ||
stxTokenContractAddresses[tokenId]?.[chainId] == null
) {
return undefined
}
const { deployerAddress, network } = contractCallInfo
return {
...stxTokenContractAddresses[tokenId]![chainId],
network,
deployerAddress,
}
}

354
src/utils/BigNumber.ts Normal file
View File

@@ -0,0 +1,354 @@
import { Big, BigSource } from "big.js"
import { OneOrMore } from "./typeHelpers"
export type BigNumberSource = number | bigint | string | Big | BigNumber
const toBig = (num: BigNumberSource): Big => {
if (num instanceof Big) {
return num
}
return new Big(num as BigSource)
}
const fromBig = (num: Big): BigNumber => {
return num as unknown as BigNumber
}
export type BigNumber = Omit<Big, keyof Big> & {
___B: "BigNumber"
}
export namespace BigNumber {
export const { roundUp, roundDown, roundHalfUp, roundHalfEven } = Big
export type RoundingMode =
| typeof roundUp
| typeof roundDown
| typeof roundHalfUp
| typeof roundHalfEven
let defaultRoundingMode: RoundingMode = roundHalfUp
export const setDefaultRoundingMode = (roundingMode: RoundingMode): void => {
defaultRoundingMode = roundingMode
}
export const isBigNumber = (num: any): num is BigNumber => {
return num instanceof Big
}
export const safeFrom = (value: BigNumberSource): undefined | BigNumber => {
try {
return from(value)
} catch (e) {
return undefined
}
}
export const from = (value: BigNumberSource): BigNumber => {
return fromBig(toBig(value as any))
}
export const toString = (value: BigNumberSource): string => {
return toBig(value).toString()
}
export const toNumber = (value: BigNumberSource): number => {
return toBig(value).toNumber()
}
export const toBigInt = curry2(
(
options: {
roundingMode?: RoundingMode
},
value: BigNumberSource,
): bigint => {
return BigInt(
toFixed(
{
precision: 0,
roundingMode: options.roundingMode ?? defaultRoundingMode,
},
toBig(value),
),
)
},
)
export const toFixed = curry2(
(
options: {
precision?: number
roundingMode?: RoundingMode
},
value: BigNumberSource,
): string => {
return toBig(value).toFixed(
options.precision,
options.roundingMode ?? defaultRoundingMode,
)
},
)
export const toExponential = curry2(
(
options: {
precision?: number
roundingMode?: RoundingMode
},
value: BigNumberSource,
): string => {
return toBig(value).toExponential(
options.precision,
options.roundingMode ?? defaultRoundingMode,
)
},
)
export const isNegative = (value: BigNumberSource): boolean => {
return toBig(value).lt(0)
}
export const isGtZero = (value: BigNumberSource): boolean => {
return toBig(value).gt(0)
}
export const isZero = (value: BigNumberSource): boolean => {
return toBig(value).eq(0)
}
export const isEq = curry2(
(value: BigNumberSource, a: BigNumberSource): boolean => {
return toBig(value).eq(toBig(a))
},
)
export const isGt = curry2(
(value: BigNumberSource, a: BigNumberSource): boolean => {
return toBig(value).gt(toBig(a))
},
)
export const isGte = curry2(
(value: BigNumberSource, a: BigNumberSource): boolean => {
return toBig(value).gte(toBig(a))
},
)
export const isLt = curry2(
(value: BigNumberSource, a: BigNumberSource): boolean => {
return toBig(value).lt(toBig(a))
},
)
export const isLte = curry2(
(value: BigNumberSource, a: BigNumberSource): boolean => {
return toBig(value).lte(toBig(a))
},
)
export const setPrecision = curry2(
(
options: {
precision?: number
roundingMode?: RoundingMode
},
value: BigNumberSource,
): BigNumber => {
return fromBig(
toBig(
toBig(value).toPrecision(
options.precision,
options.roundingMode ?? defaultRoundingMode,
),
),
)
},
)
export const getPrecision = (value: BigNumberSource): number => {
return toBig(value).c.length - (toBig(value).e + 1)
}
export const getIntegerLength = (value: BigNumberSource): number => {
return toBig(value).e + 1
}
export const leftMoveDecimals = curry2(
(distance: number, value: BigNumberSource): BigNumber =>
moveDecimals({ distance }, value),
)
export const rightMoveDecimals = curry2(
(distance: number, value: BigNumberSource): BigNumber =>
moveDecimals({ distance: -distance }, value),
)
export const moveDecimals = curry2(
(options: { distance: number }, value: BigNumberSource): BigNumber => {
if (options.distance > 0) {
return fromBig(toBig(value).div(10 ** options.distance))
}
if (options.distance < 0) {
return fromBig(toBig(value).mul(10 ** -options.distance))
}
// distance === 0
return from(value)
},
)
export const getDecimalPart = curry2(
(
options: { precision: number },
value: BigNumberSource,
): undefined | string => {
/**
* `toString` will return `"1e-8"` in some case, so we choose `toFixed` here
*/
const formatted = toFixed(
{
precision: Math.min(getPrecision(value), options.precision),
roundingMode: roundDown,
},
value,
)
const [, decimals] = formatted.split(".")
if (decimals == null) return undefined
return decimals
},
)
export const abs = (value: BigNumberSource): BigNumber => {
return fromBig(toBig(value).abs())
}
export const neg = (value: BigNumberSource): BigNumber => {
return fromBig(toBig(value).neg())
}
export const sqrt = (value: BigNumberSource): BigNumber => {
return fromBig(toBig(value).sqrt())
}
export const add = curry2(
(value: BigNumberSource, a: BigNumberSource): BigNumber => {
return fromBig(toBig(value).add(toBig(a)))
},
)
export const minus = curry2(
(value: BigNumberSource, a: BigNumberSource): BigNumber => {
return fromBig(toBig(value).minus(toBig(a)))
},
)
export const mul = curry2(
(value: BigNumberSource, a: BigNumberSource): BigNumber => {
return fromBig(toBig(value).mul(toBig(a)))
},
)
export const div = curry2(
(value: BigNumberSource, a: BigNumberSource): BigNumber => {
return fromBig(toBig(value).div(toBig(a)))
},
)
export const pow = curry2((value: BigNumberSource, a: number): BigNumber => {
return fromBig(toBig(value).pow(a))
})
export const round = curry2(
(
options: {
precision?: number
roundingMode?: RoundingMode
},
value: BigNumberSource,
): BigNumber => {
return fromBig(
toBig(value).round(
options.precision,
options.roundingMode ?? defaultRoundingMode,
),
)
},
)
export const toPrecision = curry2(
(
options: {
precision?: number
roundingMode?: RoundingMode
},
value: BigNumberSource,
): string => {
return toBig(value).toPrecision(
options.precision,
options.roundingMode ?? defaultRoundingMode,
)
},
)
export const ascend = curry2(
(a: BigNumberSource, b: BigNumberSource): -1 | 0 | 1 =>
isLt(a, b) ? -1 : isGt(a, b) ? 1 : 0,
)
export const descend = curry2(
(a: BigNumberSource, b: BigNumberSource): -1 | 0 | 1 =>
isLt(a, b) ? 1 : isGt(a, b) ? -1 : 0,
)
export const sort = (
comparator: (a: BigNumber, b: BigNumber) => -1 | 0 | 1,
numbers: readonly BigNumberSource[],
): BigNumber[] => {
const _numbers = numbers.map(a => fromBig(toBig(a)))
_numbers.sort(comparator)
return _numbers
}
export const max = (numbers: OneOrMore<BigNumberSource>): BigNumber => {
return from(sort(descend, numbers)[0]!)
}
export const min = (numbers: OneOrMore<BigNumberSource>): BigNumber => {
return from(sort(ascend, numbers)[0]!)
}
export const clamp = (
range: [min: BigNumber, max: BigNumber],
n: BigNumber,
): BigNumber => {
const [min, max] = range
if (isGte(n, max)) return max
if (isLte(n, min)) return min
return n
}
export const sum = (numbers: BigNumberSource[]): BigNumber => {
return numbers
.map(n => fromBig(toBig(n)))
.reduce((acc, n) => add(acc, n), ZERO)
}
export const ZERO = BigNumber.from(0)
}
interface Curry2<Args extends [any, any], Ret> {
(a: Args[0]): (b: Args[1]) => Ret
(a: Args[0], b: Args[1]): Ret
}
function curry2<Args extends [any, any], Ret>(
fn: (...args: Args) => Ret,
): Curry2<Args, Ret> {
return ((a: Args[0], b?: Args[1]): Ret => {
if (arguments.length > 1) {
return (fn as any)(a, b)
}
return ((b: Args[1]): Ret => (fn as any)(a, b)) as any
}) as any
}

92
src/utils/arrayHelpers.ts Normal file
View File

@@ -0,0 +1,92 @@
export function hasAny<T>(ary: T[]): ary is [T, ...T[]]
export function hasAny<T>(ary: readonly T[]): ary is readonly [T, ...T[]]
export function hasAny<T>(ary: readonly T[]): ary is readonly [T, ...T[]] {
return ary.length > 0
}
export function range(start: number, end: number): number[] {
return Array.from({ length: end - start }, (_, i) => i + start)
}
export function uniq<T>(
ary: T[],
iteratee: (item: T) => any = item => item,
): T[] {
const seen = new Set<any>()
return ary.filter(item => {
const key = iteratee(item)
if (seen.has(key)) {
return false
}
seen.add(key)
return true
})
}
export function oneOf<T extends string[]>(
...coll: T
): (input: unknown) => input is T[number]
export function oneOf<T extends any[]>(
...coll: T
): (input: unknown) => input is T[number]
export function oneOf<T extends any[]>(
...coll: T
): (input: unknown) => input is T[number] {
return ((input: unknown) => coll.includes(input)) as any
}
export type SortByIteratee<T> = (
item: T,
index: number,
ary: T[],
) => number | bigint
/**
* https://github.com/jashkenas/underscore/blob/1abc36c169947c54c97e266513b1d763d0198f46/modules/sortBy.js
*/
export function sortBy<T>(
iteratee: SortByIteratee<T> | SortByIteratee<T>[],
ary: T[],
): T[] {
const _iteratee = Array.isArray(iteratee) ? iteratee : [iteratee]
return ary
.map((value, index, ary) => ({
value: value,
index: index,
criteria: _iteratee.map(f => f(value, index, ary)),
}))
.sort((left, right) => compareMultiple(left, right))
.map(v => v.value)
}
interface CompareMultipleItem<T> {
value: T
index: number
criteria: (number | bigint)[]
}
/**
* https://github.com/lodash/lodash/blob/4.17.15/lodash.js#L4632-L4657
*/
function compareMultiple<T>(
obj: CompareMultipleItem<T>,
oth: CompareMultipleItem<T>,
): number {
const objCriteria = obj.criteria
const othCriteria = oth.criteria
const length = objCriteria.length
let index = -1
while (++index < length) {
const objCri = objCriteria[index]
const othCri = othCriteria[index]
let result = 0
if (typeof objCri === typeof othCri) {
result = Number((objCri as bigint) - (othCri as bigint))
} else {
result = Number(objCri) - Number(othCri)
}
if (result) return result
}
return obj.index - oth.index
}

View File

@@ -0,0 +1,9 @@
export const MAX_BIGINT = BigInt(Number.MAX_VALUE)
export function sum(nums: bigint[]): bigint {
return nums.reduce((acc, val) => acc + val, 0n)
}
export function max(nums: bigint[]): bigint {
return nums.reduce((acc, val) => (acc > val ? acc : val), 0n)
}

View File

@@ -0,0 +1,309 @@
import { UnsupportedBridgeRouteError } from "./errors"
import { ChainIdInternal, TokenIdInternal } from "./types.internal"
import { OneOrMore } from "./typeHelpers"
export type SupportedRoute = {
chainLeft: ChainIdInternal
chainRight: ChainIdInternal
tokenLeft: TokenIdInternal
tokenRight: TokenIdInternal
}
export function defineRoute<
ChainPair extends [chainLeft: ChainIdInternal, chainRight: ChainIdInternal],
TokenPairs extends OneOrMore<
[tokenLeft: TokenIdInternal, tokenRight: TokenIdInternal]
>,
>(
chainPair: ChainPair,
tokenPairs: TokenPairs,
): {
[K in keyof TokenPairs]: {
chainLeft: ChainPair[0]
chainRight: ChainPair[1]
tokenLeft: TokenPairs[K][0]
tokenRight: TokenPairs[K][1]
}
} {
return tokenPairs.map(tokenPair => ({
chainLeft: chainPair[0],
chainRight: chainPair[1],
tokenLeft: tokenPair[0],
tokenRight: tokenPair[1],
})) as any
}
type ExtractRouteLtr<
Routes extends SupportedRoute,
FromChain extends ChainIdInternal,
ToChain extends ChainIdInternal,
> = Routes extends {
chainLeft: FromChain
chainRight: ToChain
tokenLeft: infer _FromToken
tokenRight: infer _ToToken
}
? {
fromChain: FromChain
toChain: ToChain
fromToken: _FromToken
toToken: _ToToken
}
: never
type ExtractRouteRtl<
Routes extends SupportedRoute,
FromChain extends ChainIdInternal,
ToChain extends ChainIdInternal,
> = Routes extends {
chainRight: FromChain
chainLeft: ToChain
tokenRight: infer _FromToken
tokenLeft: infer _ToToken
}
? {
fromChain: FromChain
toChain: ToChain
fromToken: _FromToken
toToken: _ToToken
}
: never
export type GetSupportedRoutesFnAnyResult = {
fromChain: ChainIdInternal
toChain: ChainIdInternal
fromToken: TokenIdInternal
toToken: TokenIdInternal
}[]
export type GetSupportedTokensFn<Routes extends SupportedRoute> = <
FromChain extends ChainIdInternal,
ToChain extends ChainIdInternal,
>(
fromChain: FromChain,
toChain: ToChain,
) => Promise<
(
| ExtractRouteLtr<Routes, FromChain, ToChain>
| ExtractRouteRtl<Routes, FromChain, ToChain>
)[]
>
export type PickLeftToRightRouteOrThrowFn<Routes extends SupportedRoute> = <
FromChain extends ChainIdInternal,
ToChain extends ChainIdInternal,
>(
fromChain: FromChain,
toChain: ToChain,
fromToken: TokenIdInternal,
toToken: TokenIdInternal,
) => Promise<
// prettier-ignore
Routes extends { chainLeft: infer FromChain; chainRight: infer ToChain }
? { fromChain: FromChain; toChain: ToChain }
: never
>
export type PickRightToLeftRouteOrThrowFn<Routes extends SupportedRoute> = <
FromChain extends ChainIdInternal,
ToChain extends ChainIdInternal,
>(
fromChain: FromChain,
toChain: ToChain,
fromToken: TokenIdInternal,
toToken: TokenIdInternal,
) => Promise<
// prettier-ignore
Routes extends { chainRight: infer FromChain; chainLeft: infer ToChain }
? { fromChain: FromChain; toChain: ToChain }
: never
>
export interface BuiltSupportedRoutes<SR extends SupportedRoute> {
supportedRoutes: SR[]
getSupportedTokens: GetSupportedTokensFn<SR>
pickLeftToRightRouteOrThrow: PickLeftToRightRouteOrThrowFn<SR>
pickRightToLeftRouteOrThrow: PickRightToLeftRouteOrThrowFn<SR>
}
export type SupportedRoutesOf<T extends BuiltSupportedRoutes<any>> =
T["supportedRoutes"][number]
export type IsAvailableFn<SR extends SupportedRoute> = (
route: SR extends {
chainLeft: infer ChainLeft
chainRight: infer ChainRight
tokenLeft: infer TokenLeft
tokenRight: infer TokenRight
}
?
| {
fromChain: ChainLeft
toChain: ChainRight
fromToken: TokenLeft
toToken: TokenRight
}
| {
fromChain: ChainRight
toChain: ChainLeft
fromToken: TokenRight
toToken: TokenLeft
}
: never,
) => Promise<boolean>
export function buildSupportedRoutes<SRs extends SupportedRoute[]>(
routes: SRs[],
options: {
isAvailable?: IsAvailableFn<SRs[number]>
} = {},
): BuiltSupportedRoutes<SRs[number]> {
const _routes = routes.flat()
const isAvailable = options.isAvailable || (() => Promise.resolve(true))
const getSupportedTokens = getSupportedTokensFactory(_routes, isAvailable)
const pickLeftToRightRouteOrThrow = pickLeftToRightRouteOrThrowFactory(
_routes,
isAvailable,
)
const pickRightToLeftRouteOrThrow = pickRightToLeftRouteOrThrowFactory(
_routes,
isAvailable,
)
return {
supportedRoutes: _routes,
getSupportedTokens,
pickLeftToRightRouteOrThrow,
pickRightToLeftRouteOrThrow,
}
}
const pickRightToLeftRouteOrThrowFactory =
<SR extends SupportedRoute>(
routes: SR[],
isAvailable: IsAvailableFn<SR>,
): PickRightToLeftRouteOrThrowFn<SR> =>
async (fromChain, toChain, fromToken, toToken) => {
let result:
| undefined
| { fromChain: ChainIdInternal; toChain: ChainIdInternal } = undefined
for (const r of routes) {
if (
r.chainRight === fromChain &&
r.chainLeft === toChain &&
r.tokenRight === fromToken &&
r.tokenLeft === toToken &&
(await isAvailable({
fromChain,
toChain,
fromToken,
toToken,
} as any))
) {
result = { fromChain, toChain }
break
}
if (result) return result as any
}
throw new UnsupportedBridgeRouteError(
fromChain,
toChain,
fromToken,
toToken,
)
}
const pickLeftToRightRouteOrThrowFactory =
<SR extends SupportedRoute>(
routes: SR[],
isAvailable: IsAvailableFn<SR>,
): PickLeftToRightRouteOrThrowFn<SR> =>
async (fromChain, toChain, fromToken, toToken) => {
let result:
| undefined
| { fromChain: ChainIdInternal; toChain: ChainIdInternal } = undefined
for (const r of routes) {
if (
r.chainLeft === fromChain &&
r.chainRight === toChain &&
r.tokenLeft === fromToken &&
r.tokenRight === toToken &&
(await isAvailable({
fromChain,
toChain,
fromToken,
toToken,
} as any))
) {
result = { fromChain, toChain }
break
}
if (result) return result as any
}
throw new UnsupportedBridgeRouteError(
fromChain,
toChain,
fromToken,
toToken,
)
}
const getSupportedTokensFactory =
<SR extends SupportedRoute>(
routes: SR[],
isAvailable: IsAvailableFn<SR>,
): GetSupportedTokensFn<SR> =>
async (fromChain, toChain) => {
const promises = routes.map(
async (
r,
): Promise<
{
fromChain: ChainIdInternal
toChain: ChainIdInternal
fromToken: TokenIdInternal
toToken: TokenIdInternal
}[]
> => {
let route:
| undefined
| {
fromChain: ChainIdInternal
toChain: ChainIdInternal
fromToken: TokenIdInternal
toToken: TokenIdInternal
} = undefined
if (r.chainLeft === fromChain && r.chainRight === toChain) {
route = {
fromChain: r.chainLeft,
toChain: r.chainRight,
fromToken: r.tokenLeft,
toToken: r.tokenRight,
}
}
if (r.chainLeft === toChain && r.chainRight === fromChain) {
route = {
fromChain: r.chainRight,
toChain: r.chainLeft,
fromToken: r.tokenRight,
toToken: r.tokenLeft,
}
}
return route == null || !(await isAvailable(route as any))
? []
: [route]
},
)
return Promise.all(promises).then(results => results.flat() as any)
}

49
src/utils/errors.ts Normal file
View File

@@ -0,0 +1,49 @@
import { TxBroadcastResultRejected } from "@stacks/transactions"
import { ChainId, TokenId } from "../xlinkSdkUtils/types"
import { ChainIdInternal, TokenIdInternal } from "./types.internal"
export class XLINKSDKErrorBase extends Error {
constructor(message: string) {
super(message)
this.name = "XLINKSDKErrorBase"
}
}
export class UnsupportedBridgeRouteError extends XLINKSDKErrorBase {
fromChain: ChainId
toChain: ChainId
fromToken: TokenId
toToken?: TokenId
constructor(
fromChain: ChainId | ChainIdInternal,
toChain: ChainId | ChainIdInternal,
fromToken: TokenId | TokenIdInternal,
toToken?: TokenId | TokenIdInternal,
) {
super(
`Unsupported chain combination: ${fromToken}(${fromChain}) -> ${toToken ?? "Unknown Token"}(${toChain})`,
)
this.name = "UnsupportedBridgeRouteError"
this.fromChain = ChainIdInternal.toChainId(fromChain)
this.toChain = ChainIdInternal.toChainId(toChain)
this.fromToken = TokenIdInternal.toTokenId(fromToken)
this.toToken = toToken ? TokenIdInternal.toTokenId(toToken) : undefined
}
}
export class UnsupportedChainError extends XLINKSDKErrorBase {
constructor(public chainId: ChainId) {
super(`Unsupported chain: ${chainId}`)
this.name = "UnsupportedChainError"
}
}
export class StacksTransactionBroadcastError extends XLINKSDKErrorBase {
constructor(public cause: TxBroadcastResultRejected) {
super("Failed to Stacks broadcast transaction")
this.name = "StacksTransactionBroadcastError"
this.cause = cause
}
}

57
src/utils/hexHelpers.ts Normal file
View File

@@ -0,0 +1,57 @@
import { XLINKSDKErrorBase } from "./errors"
/**
* https://github.com/wevm/viem/blob/d2f93e726df1ab1ff86098d68a4406f6fae315b8/src/utils/encoding/toBytes.ts#L150-L175
*/
export function decodeHex(hex: string): Uint8Array {
let hexString = hex.startsWith("0x") ? hex.slice(2) : hex
if (hexString.length % 2) hexString = `0${hexString}`
const length = hexString.length / 2
const bytes = new Uint8Array(length)
for (let index = 0, j = 0; index < length; index++) {
const nibbleLeft = charCodeToBase16(hexString.charCodeAt(j++))
const nibbleRight = charCodeToBase16(hexString.charCodeAt(j++))
if (nibbleLeft === undefined || nibbleRight === undefined) {
throw new XLINKSDKErrorBase(
`Invalid byte sequence ("${hexString[j - 2]}${
hexString[j - 1]
}" in "${hexString}").`,
)
}
bytes[index] = nibbleLeft * 16 + nibbleRight
}
return bytes
}
const charCodeMap = {
zero: 48,
nine: 57,
A: 65,
F: 70,
a: 97,
f: 102,
} as const
function charCodeToBase16(char: number): undefined | number {
if (char >= charCodeMap.zero && char <= charCodeMap.nine)
return char - charCodeMap.zero
if (char >= charCodeMap.A && char <= charCodeMap.F)
return char - (charCodeMap.A - 10)
if (char >= charCodeMap.a && char <= charCodeMap.f)
return char - (charCodeMap.a - 10)
return undefined
}
/**
* https://github.com/wevm/viem/blob/d2f93e726df1ab1ff86098d68a4406f6fae315b8/src/utils/encoding/toHex.ts#L131-L143
*/
export function encodeHex(value: Uint8Array): string {
let string = ""
for (let i = 0; i < value.length; i++) {
string += hexes[value[i]]
}
const hex = `0x${string}` as const
return hex
}
const hexes = Array.from({ length: 256 }, (_v, i) =>
i.toString(16).padStart(2, "0"),
)

146
src/utils/isPlainObject.ts Normal file
View File

@@ -0,0 +1,146 @@
/**
* lodash (Custom Build) <https://lodash.com/>
* Build: `lodash modularize exports="npm" -o ./`
* Copyright jQuery Foundation and other contributors <https://jquery.org/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
/** `Object#toString` result references. */
const objectTag = "[object Object]"
/**
* Checks if `value` is a host object in IE < 9.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a host object, else `false`.
*/
function isHostObject(value: any): boolean {
// Many host objects are `Object` objects that can coerce to strings
// despite having improperly defined `toString` methods.
let result = false
if (value != null && typeof value.toString != "function") {
try {
result = !!(value + "")
} catch (e) {}
}
return result
}
/**
* Creates a unary function that invokes `func` with its argument transformed.
*
* @private
* @param {Function} func The function to wrap.
* @param {Function} transform The argument transform.
* @returns {Function} Returns the new function.
*/
function overArg<A1, A2, R1>(
func: (arg: A2) => R1,
transform: (arg: A1) => A2,
): (arg: A1) => R1 {
return function (arg: A1): R1 {
return func(transform(arg))
}
}
/** Used for built-in method references. */
const funcProto = Function.prototype,
objectProto = Object.prototype
/** Used to resolve the decompiled source of functions. */
const funcToString = funcProto.toString
/** Used to check objects for own properties. */
const hasOwnProperty = objectProto.hasOwnProperty
/** Used to infer the `Object` constructor. */
const objectCtorString = funcToString.call(Object)
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
* of values.
*/
const objectToString = objectProto.toString
/** Built-in value references. */
const getPrototype = overArg(Object.getPrototypeOf, Object)
/**
* Checks if `value` is object-like. A value is object-like if it's not `null`
* and has a `typeof` result of "object".
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is object-like, else `false`.
* @example
*
* _.isObjectLike({});
* // => true
*
* _.isObjectLike([1, 2, 3]);
* // => true
*
* _.isObjectLike(_.noop);
* // => false
*
* _.isObjectLike(null);
* // => false
*/
function isObjectLike(value: any): boolean {
return !!value && typeof value == "object"
}
/**
* Checks if `value` is a plain object, that is, an object created by the
* `Object` constructor or one with a `[[Prototype]]` of `null`.
*
* @static
* @memberOf _
* @since 0.8.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
* @example
*
* function Foo() {
* this.a = 1;
* }
*
* _.isPlainObject(new Foo);
* // => false
*
* _.isPlainObject([1, 2, 3]);
* // => false
*
* _.isPlainObject({ 'x': 0, 'y': 0 });
* // => true
*
* _.isPlainObject(Object.create(null));
* // => true
*/
export function isPlainObject(value: any): boolean {
if (
!isObjectLike(value) ||
objectToString.call(value) != objectTag ||
isHostObject(value)
) {
return false
}
const proto = getPrototype(value)
if (proto === null) {
return true
}
const Ctor = hasOwnProperty.call(proto, "constructor") && proto.constructor
return (
typeof Ctor == "function" &&
Ctor instanceof Ctor &&
funcToString.call(Ctor) == objectCtorString
)
}

18
src/utils/typeHelpers.ts Normal file
View File

@@ -0,0 +1,18 @@
export function isNotNull<T>(input: T | undefined | null): input is T {
return input != null
}
export function checkNever(_x: never): undefined {
/* do nothing */
return
}
export type StringOnly<T> = Extract<T, string>
export type NumberOnly<T> = Extract<T, number>
export type OneOrMore<T> = readonly [T, ...T[]]
export type CompactType<T> = {
[P in keyof T]: T[P]
}

View File

@@ -0,0 +1,94 @@
import { ChainId, TokenId } from "../xlinkSdkUtils/types"
import { oneOf } from "./arrayHelpers"
export type ChainIdInternal = string
export namespace ChainIdInternal {
export const toChainId = (value: ChainIdInternal): ChainId => value as ChainId
}
export type TokenIdInternal = string
export namespace TokenIdInternal {
export const toTokenId = (value: TokenIdInternal): TokenId => value as TokenId
}
const chainId = <const T extends ChainIdInternal>(value: T): T => value
const tokenId = <const T extends TokenIdInternal>(value: T): T => value
export namespace KnownTokenId {
export namespace Bitcoin {
export const BTC = tokenId("btc-btc")
}
export namespace Ethereum {
export const WBTC = tokenId("eth-wbtc")
export const BTCB = tokenId("eth-btcb")
export const USDT = tokenId("eth-usdt")
export const LUNR = tokenId("eth-lunr")
export const ALEX = tokenId("eth-alex")
export const SKO = tokenId("eth-sko")
}
export namespace Stacks {
export const aBTC = tokenId("stx-abtc")
export const sUSDT = tokenId("stx-susdt")
export const sLUNR = tokenId("stx-slunr")
export const ALEX = tokenId("stx-alex")
export const sSKO = tokenId("stx-ssko")
}
}
export namespace KnownChainId {
export namespace Bitcoin {
export const Mainnet = chainId("bitcoin-mainnet")
export const Testnet = chainId("bitcoin-testnet")
}
const bitcoinChains = [Bitcoin.Mainnet, Bitcoin.Testnet] as const
export type BitcoinChain = (typeof bitcoinChains)[number]
export function isBitcoinChain(
value: ChainIdInternal,
): value is BitcoinChain {
return (
value === KnownChainId.Bitcoin.Mainnet ||
value === KnownChainId.Bitcoin.Testnet
)
}
export namespace Ethereum {
// mainnet
export const Mainnet = chainId("ethereum-mainnet")
export const BSC = chainId("ethereum-bsc")
// export const AVAX = chainId("ethereum-avax")
// export const Polygon = chainId("ethereum-polygon")
// testnet
export const Sepolia = chainId("ethereum-sepolia")
export const BSCTest = chainId("ethereum-bsctestnet")
}
const ethereumChains = [
Ethereum.Mainnet,
Ethereum.BSC,
// Ethereum.AVAX,
// Ethereum.Polygon,
Ethereum.Sepolia,
Ethereum.BSCTest,
] as const
export type EthereumChain = (typeof ethereumChains)[number]
export function isEthereumChain(
value: ChainIdInternal,
): value is EthereumChain {
return oneOf(...ethereumChains)(value)
}
export namespace Stacks {
export const Mainnet = chainId("stacks-mainnet")
export const Testnet = chainId("stacks-testnet")
}
const stacksChains = [Stacks.Mainnet, Stacks.Testnet] as const
export type StacksChain = (typeof stacksChains)[number]
export function isStacksChain(value: ChainIdInternal): value is StacksChain {
return (
value === KnownChainId.Stacks.Mainnet ||
value === KnownChainId.Stacks.Testnet
)
}
}

View File

@@ -0,0 +1,243 @@
import * as btc from "@scure/btc-signer"
import { bitcoinToSatoshi } from "../bitcoinUtils/bitcoinHelpers"
import { broadcastSignedTransaction } from "../bitcoinUtils/broadcastSignedTransaction"
import { getBTCPegInAddress } from "../bitcoinUtils/btcAddresses"
import { createTransaction } from "../bitcoinUtils/createTransaction"
import {
ReselectSpendableUTXOsFn,
prepareTransaction,
} from "../bitcoinUtils/prepareTransaction"
import { createBridgeOrder_BitcoinToStack } from "../stacksUtils/createBridgeOrder"
import { validateBridgeOrder_BitcoinToStack } from "../stacksUtils/validateBridgeOrder"
import {
getContractCallInfo,
numberToStacksContractNumber,
} from "../stacksUtils/xlinkContractHelpers"
import {
GetSupportedRoutesFnAnyResult,
buildSupportedRoutes,
defineRoute,
} from "../utils/buildSupportedRoutes"
import { UnsupportedBridgeRouteError } from "../utils/errors"
import { checkNever } from "../utils/typeHelpers"
import { KnownChainId, KnownTokenId } from "../utils/types.internal"
import { ChainId } from "./types"
export const supportedRoutes = buildSupportedRoutes(
[
// from mainnet
defineRoute(
[KnownChainId.Bitcoin.Mainnet, KnownChainId.Stacks.Mainnet],
[[KnownTokenId.Bitcoin.BTC, KnownTokenId.Stacks.aBTC]],
),
// defineRoute(
// [KnownChainId.Bitcoin.Mainnet, KnownChainId.Ethereum.Mainnet],
// [[KnownTokenId.Bitcoin.BTC, KnownTokenId.Ethereum.WBTC]],
// ),
// defineRoute(
// [KnownChainId.Bitcoin.Mainnet, KnownChainId.Ethereum.BSC],
// [[KnownTokenId.Bitcoin.BTC, KnownTokenId.Ethereum.BTCB]],
// ),
// from testnet
defineRoute(
[KnownChainId.Bitcoin.Testnet, KnownChainId.Stacks.Testnet],
[[KnownTokenId.Bitcoin.BTC, KnownTokenId.Stacks.aBTC]],
),
// defineRoute(
// [KnownChainId.Bitcoin.Testnet, KnownChainId.Ethereum.Sepolia],
// [[KnownTokenId.Bitcoin.BTC, KnownTokenId.Ethereum.WBTC]],
// ),
// defineRoute(
// [KnownChainId.Bitcoin.Testnet, KnownChainId.Ethereum.BSCTest],
// [[KnownTokenId.Bitcoin.BTC, KnownTokenId.Ethereum.BTCB]],
// ),
],
{
async isAvailable(route) {
const { fromChain } = route
if (
fromChain === KnownChainId.Bitcoin.Mainnet ||
fromChain === KnownChainId.Bitcoin.Testnet
) {
return !!getBTCPegInAddress(fromChain)
}
return false
},
},
)
export interface BridgeFromBitcoinInput {
fromChain: ChainId
toChain: ChainId
fromAddress: string
toAddress: string
amount: string
networkFeeRate: bigint
reselectSpendableUTXOs: ReselectSpendableUTXOsFn
signTransaction: (tx: { psbt: Uint8Array }) => Promise<{
transaction: Uint8Array
}>
}
export interface BridgeFromBitcoinOutput {
txid: string
}
export async function bridgeFromBitcoin(
info: BridgeFromBitcoinInput,
): Promise<BridgeFromBitcoinOutput> {
const res: GetSupportedRoutesFnAnyResult =
await supportedRoutes.getSupportedTokens(info.fromChain, info.toChain)
if (res.length <= 0) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
KnownTokenId.Bitcoin.BTC,
)
}
const route = await supportedRoutes.pickLeftToRightRouteOrThrow(
info.fromChain,
info.toChain,
KnownTokenId.Bitcoin.BTC,
res[0].toToken,
)
if (
route.fromChain === KnownChainId.Bitcoin.Mainnet ||
route.fromChain === KnownChainId.Bitcoin.Testnet
) {
if (
route.toChain === KnownChainId.Stacks.Mainnet ||
route.toChain === KnownChainId.Stacks.Testnet
) {
return bridgeFromBitcoin_toStacks({
...info,
fromChain: route.fromChain,
toChain: route.toChain,
})
}
// if (
// route.toChain === KnownChainId.Ethereum.Mainnet ||
// route.toChain === KnownChainId.Ethereum.Sepolia ||
// route.toChain === KnownChainId.Ethereum.BNBMainnet ||
// route.toChain === KnownChainId.Ethereum.BNBTestnet
// ) {
// return bridgeFromBitcoin_toEthereum({
// ...info,
// fromChain: route.fromChain,
// toChain: route.toChain,
// })
// }
checkNever(route)
} else {
checkNever(route)
}
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
KnownTokenId.Bitcoin.BTC,
res[0].toToken,
)
}
async function bridgeFromBitcoin_toStacks(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: Omit<BridgeFromBitcoinInput, "fromChain" | "toChain"> & {
fromChain:
| typeof KnownChainId.Bitcoin.Mainnet
| typeof KnownChainId.Bitcoin.Testnet
toChain:
| typeof KnownChainId.Stacks.Mainnet
| typeof KnownChainId.Stacks.Testnet
},
): Promise<BridgeFromBitcoinOutput> {
const pegInAddress = getBTCPegInAddress(info.fromChain)
const contractCallInfo = getContractCallInfo(info.toChain)
if (contractCallInfo == null || pegInAddress == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
KnownTokenId.Bitcoin.BTC,
)
}
const bitcoinNetwork =
info.fromChain === KnownChainId.Bitcoin.Mainnet
? btc.NETWORK
: btc.TEST_NETWORK
const { data: opReturnData } = await createBridgeOrder_BitcoinToStack({
network: contractCallInfo.network,
endpointDeployerAddress: contractCallInfo.deployerAddress,
receiverStxAddr: info.toAddress,
swapSlippedAmount: numberToStacksContractNumber(info.amount),
swapRoute: [],
})
const txOptions = await prepareTransaction({
network: bitcoinNetwork,
recipients: [
{
address: pegInAddress.address,
satsAmount: bitcoinToSatoshi(info.amount),
},
],
changeAddress: info.fromAddress,
opReturnData: [opReturnData],
feeRate: info.networkFeeRate,
reselectSpendableUTXOs: info.reselectSpendableUTXOs,
})
const tx = createTransaction(
bitcoinNetwork,
txOptions.inputs,
txOptions.recipients.concat({
address: info.fromAddress,
satsAmount: txOptions.changeAmount,
}),
[opReturnData],
)
await validateBridgeOrder_BitcoinToStack({
network: contractCallInfo.network,
endpointDeployerAddress: contractCallInfo.deployerAddress,
btcTx: tx.toBytes(true, true),
swapRoute: [],
})
const { transaction } = await info.signTransaction({
psbt: tx.toPSBT(),
})
const { txId } = await broadcastSignedTransaction(
info.fromChain === KnownChainId.Bitcoin.Mainnet ? "mainnet" : "testnet",
transaction,
)
return { txid: txId }
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function bridgeFromBitcoin_toEthereum(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: Omit<BridgeFromBitcoinInput, "fromChain" | "toChain"> & {
fromChain:
| typeof KnownChainId.Bitcoin.Mainnet
| typeof KnownChainId.Bitcoin.Testnet
toChain:
| typeof KnownChainId.Ethereum.Mainnet
| typeof KnownChainId.Ethereum.Sepolia
| typeof KnownChainId.Ethereum.BSC
| typeof KnownChainId.Ethereum.BSCTest
},
): Promise<BridgeFromBitcoinOutput> {
// TODO
return { txid: "" }
}

View File

@@ -0,0 +1,227 @@
import { Address, Hex, encodeFunctionData } from "viem"
import { sendRawTransaction } from "viem/actions"
import { bridgeEndpointAbi } from "../ethereumUtils/contractAbi/bridgeEndpoint"
import {
ethEndpointContractAddresses,
ethTokenContractAddresses,
} from "../ethereumUtils/ethContractAddresses"
import {
getContractCallInfo,
getTokenContractInfo,
numberToSolidityContractNumber,
} from "../ethereumUtils/xlinkContractHelpers"
import {
buildSupportedRoutes,
defineRoute,
} from "../utils/buildSupportedRoutes"
import { decodeHex } from "../utils/hexHelpers"
import { UnsupportedBridgeRouteError } from "../utils/errors"
import { checkNever } from "../utils/typeHelpers"
import { KnownChainId, KnownTokenId } from "../utils/types.internal"
import { ChainId, TokenId } from "./types"
export const supportedRoutes = buildSupportedRoutes(
[
// from mainnet
defineRoute(
[KnownChainId.Ethereum.Mainnet, KnownChainId.Stacks.Mainnet],
[
[KnownTokenId.Ethereum.WBTC, KnownTokenId.Stacks.aBTC],
[KnownTokenId.Ethereum.USDT, KnownTokenId.Stacks.sUSDT],
[KnownTokenId.Ethereum.LUNR, KnownTokenId.Stacks.sLUNR],
[KnownTokenId.Ethereum.ALEX, KnownTokenId.Stacks.ALEX],
],
),
// defineRoute(
// [KnownChainId.Ethereum.Mainnet, KnownChainId.Bitcoin.Mainnet],
// [[KnownTokenId.Ethereum.WBTC, KnownTokenId.Bitcoin.BTC]],
// ),
defineRoute(
[KnownChainId.Ethereum.BSC, KnownChainId.Stacks.Mainnet],
[
[KnownTokenId.Ethereum.BTCB, KnownTokenId.Stacks.aBTC],
[KnownTokenId.Ethereum.USDT, KnownTokenId.Stacks.sUSDT],
[KnownTokenId.Ethereum.LUNR, KnownTokenId.Stacks.sLUNR],
[KnownTokenId.Ethereum.ALEX, KnownTokenId.Stacks.ALEX],
],
),
// defineRoute(
// [KnownChainId.Ethereum.BSCTest, KnownChainId.Bitcoin.Mainnet],
// [[KnownTokenId.Ethereum.BTCB, KnownTokenId.Bitcoin.BTC]],
// ),
// from testnet
defineRoute(
[KnownChainId.Ethereum.Sepolia, KnownChainId.Stacks.Testnet],
[
[KnownTokenId.Ethereum.WBTC, KnownTokenId.Stacks.aBTC],
[KnownTokenId.Ethereum.USDT, KnownTokenId.Stacks.sUSDT],
[KnownTokenId.Ethereum.LUNR, KnownTokenId.Stacks.sLUNR],
[KnownTokenId.Ethereum.ALEX, KnownTokenId.Stacks.ALEX],
],
),
// defineRoute(
// [KnownChainId.Ethereum.Sepolia, KnownChainId.Bitcoin.Testnet],
// [[KnownTokenId.Ethereum.WBTC, KnownTokenId.Bitcoin.BTC]],
// ),
defineRoute(
[KnownChainId.Ethereum.BSCTest, KnownChainId.Stacks.Testnet],
[
[KnownTokenId.Ethereum.BTCB, KnownTokenId.Stacks.aBTC],
[KnownTokenId.Ethereum.USDT, KnownTokenId.Stacks.sUSDT],
[KnownTokenId.Ethereum.LUNR, KnownTokenId.Stacks.sLUNR],
[KnownTokenId.Ethereum.ALEX, KnownTokenId.Stacks.ALEX],
],
),
// defineRoute(
// [KnownChainId.Ethereum.BNBTestnet, KnownChainId.Bitcoin.Testnet],
// [[KnownTokenId.Ethereum.BTCB, KnownTokenId.Bitcoin.BTC]],
// ),
],
{
async isAvailable(route) {
if (
route.fromChain === KnownChainId.Ethereum.Mainnet ||
route.fromChain === KnownChainId.Ethereum.BSC ||
route.fromChain === KnownChainId.Ethereum.Sepolia ||
route.fromChain === KnownChainId.Ethereum.BSCTest
) {
return (
ethTokenContractAddresses[route.fromToken][route.fromChain] != null
)
}
if (
route.toChain === KnownChainId.Ethereum.Mainnet ||
route.toChain === KnownChainId.Ethereum.BSC ||
route.toChain === KnownChainId.Ethereum.Sepolia ||
route.toChain === KnownChainId.Ethereum.BSCTest
) {
return ethTokenContractAddresses[route.toToken][route.toChain] != null
}
checkNever(route)
return false
},
},
)
export interface BridgeFromEthereumInput {
fromChain: ChainId
toChain: ChainId
fromToken: TokenId
toToken: TokenId
toAddress: string
amount: string
signTransaction: (tx: { to: Address; data: Uint8Array }) => Promise<{
transactionHex: string
}>
}
export interface BridgeFromEthereumOutput {
txid: string
}
export async function bridgeFromEthereum(
info: BridgeFromEthereumInput,
): Promise<BridgeFromEthereumOutput> {
const route = await supportedRoutes.pickLeftToRightRouteOrThrow(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
if (
route.fromChain === KnownChainId.Ethereum.Mainnet ||
route.fromChain === KnownChainId.Ethereum.BSC ||
route.fromChain === KnownChainId.Ethereum.Sepolia ||
route.fromChain === KnownChainId.Ethereum.BSCTest
) {
if (
route.toChain === KnownChainId.Stacks.Mainnet ||
route.toChain === KnownChainId.Stacks.Testnet
) {
return bridgeFromEthereum_toStacks({
...info,
fromChain: route.fromChain,
toChain: route.toChain,
})
}
// if (KnownChainId.isBitcoinChain(route.toChain)) {
// return bridgeFromEthereum_toBitcoin({
// ...info,
// fromChain: route.fromChain,
// toChain: route.toChain,
// })
// }
checkNever(route)
} else {
checkNever(route)
}
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
async function bridgeFromEthereum_toStacks(
info: Omit<BridgeFromEthereumInput, "fromChain" | "toChain"> & {
fromChain: KnownChainId.EthereumChain
toChain: KnownChainId.StacksChain
},
): Promise<BridgeFromEthereumOutput> {
const bridgeEndpointAddress =
ethEndpointContractAddresses.bridgeEndpoint[info.fromChain]
const contractCallInfo = getContractCallInfo(info.fromChain)
const fromTokenContractAddress = getTokenContractInfo(
info.fromChain,
info.fromToken,
)
if (contractCallInfo == null || fromTokenContractAddress == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
const functionData = await encodeFunctionData({
abi: bridgeEndpointAbi,
functionName: "transferToWrap",
args: [
fromTokenContractAddress.contractAddress,
numberToSolidityContractNumber(info.amount),
info.toAddress,
],
})
const { transactionHex } = await info.signTransaction({
to: bridgeEndpointAddress,
data: decodeHex(functionData),
})
const txid = await sendRawTransaction(contractCallInfo.client, {
serializedTransaction: transactionHex as Hex,
})
return { txid }
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function bridgeFromEthereum_toBitcoin(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: Omit<BridgeFromEthereumInput, "fromChain" | "toChain"> & {
fromChain: KnownChainId.EthereumChain
toChain: KnownChainId.BitcoinChain
},
): Promise<BridgeFromEthereumOutput> {
// TODO
return { txid: "" }
}

View File

@@ -0,0 +1,272 @@
import * as btc from "@scure/btc-signer"
import {
broadcastTransaction,
deserializeTransaction,
} from "@stacks/transactions"
import { ContractCallOptions } from "clarity-codegen"
import { addressToScriptPubKey } from "../bitcoinUtils/bitcoinHelpers"
import { contractAssignedChainIdFromBridgeChain } from "../ethereumUtils/crossContractDataMapping"
import {
composeTxXLINK,
getContractCallInfo,
getTokenContractInfo,
numberToStacksContractNumber,
} from "../stacksUtils/xlinkContractHelpers"
import {
buildSupportedRoutes,
defineRoute,
} from "../utils/buildSupportedRoutes"
import { decodeHex } from "../utils/hexHelpers"
import {
StacksTransactionBroadcastError,
UnsupportedBridgeRouteError,
} from "../utils/errors"
import { checkNever } from "../utils/typeHelpers"
import { KnownChainId, KnownTokenId } from "../utils/types.internal"
import { ChainId, TokenId } from "./types"
import { stxTokenContractAddresses } from "../stacksUtils/stxContractAddresses"
export const supportedRoutes = buildSupportedRoutes(
[
// from mainnet
defineRoute(
[KnownChainId.Stacks.Mainnet, KnownChainId.Bitcoin.Mainnet],
[[KnownTokenId.Stacks.aBTC, KnownTokenId.Bitcoin.BTC]],
),
defineRoute(
[KnownChainId.Stacks.Mainnet, KnownChainId.Ethereum.Mainnet],
[
[KnownTokenId.Stacks.aBTC, KnownTokenId.Ethereum.WBTC],
[KnownTokenId.Stacks.sUSDT, KnownTokenId.Ethereum.USDT],
[KnownTokenId.Stacks.sLUNR, KnownTokenId.Ethereum.LUNR],
[KnownTokenId.Stacks.ALEX, KnownTokenId.Ethereum.ALEX],
[KnownTokenId.Stacks.sSKO, KnownTokenId.Ethereum.SKO],
],
),
defineRoute(
[KnownChainId.Stacks.Mainnet, KnownChainId.Ethereum.BSC],
[
[KnownTokenId.Stacks.aBTC, KnownTokenId.Ethereum.BTCB],
[KnownTokenId.Stacks.sUSDT, KnownTokenId.Ethereum.USDT],
[KnownTokenId.Stacks.sLUNR, KnownTokenId.Ethereum.LUNR],
[KnownTokenId.Stacks.ALEX, KnownTokenId.Ethereum.ALEX],
[KnownTokenId.Stacks.sSKO, KnownTokenId.Ethereum.SKO],
],
),
// from testnet
defineRoute(
[KnownChainId.Stacks.Testnet, KnownChainId.Bitcoin.Testnet],
[[KnownTokenId.Stacks.aBTC, KnownTokenId.Bitcoin.BTC]],
),
defineRoute(
[KnownChainId.Stacks.Testnet, KnownChainId.Ethereum.Sepolia],
[
[KnownTokenId.Stacks.aBTC, KnownTokenId.Ethereum.WBTC],
[KnownTokenId.Stacks.sUSDT, KnownTokenId.Ethereum.USDT],
[KnownTokenId.Stacks.sLUNR, KnownTokenId.Ethereum.LUNR],
[KnownTokenId.Stacks.ALEX, KnownTokenId.Ethereum.ALEX],
[KnownTokenId.Stacks.sSKO, KnownTokenId.Ethereum.SKO],
],
),
defineRoute(
[KnownChainId.Stacks.Testnet, KnownChainId.Ethereum.BSCTest],
[
[KnownTokenId.Stacks.aBTC, KnownTokenId.Ethereum.BTCB],
[KnownTokenId.Stacks.sUSDT, KnownTokenId.Ethereum.USDT],
[KnownTokenId.Stacks.sLUNR, KnownTokenId.Ethereum.LUNR],
[KnownTokenId.Stacks.ALEX, KnownTokenId.Ethereum.ALEX],
[KnownTokenId.Stacks.sSKO, KnownTokenId.Ethereum.SKO],
],
),
],
{
async isAvailable(route) {
if (
route.fromChain === KnownChainId.Stacks.Mainnet ||
route.fromChain === KnownChainId.Stacks.Testnet
) {
return (
stxTokenContractAddresses[route.fromToken]?.[route.fromChain] != null
)
}
if (
route.toChain === KnownChainId.Stacks.Mainnet ||
route.toChain === KnownChainId.Stacks.Testnet
) {
return stxTokenContractAddresses[route.toToken]?.[route.toChain] != null
}
checkNever(route)
return false
},
},
)
export interface BridgeFromStacksInput {
fromChain: ChainId
toChain: ChainId
fromToken: TokenId
toToken: TokenId
toAddress: string
amount: string
signTransaction: (tx: ContractCallOptions) => Promise<{
transactionHex: string
}>
}
export interface BridgeFromStacksOutput {
txid: string
}
export async function bridgeFromStacks(
info: BridgeFromStacksInput,
): Promise<BridgeFromStacksOutput> {
const route = await supportedRoutes.pickLeftToRightRouteOrThrow(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
if (
route.fromChain === KnownChainId.Stacks.Mainnet ||
route.fromChain === KnownChainId.Stacks.Testnet
) {
if (
route.toChain === KnownChainId.Bitcoin.Mainnet ||
route.toChain === KnownChainId.Bitcoin.Testnet
) {
return bridgeFromStacks_toBitcoin({
...info,
fromChain: route.fromChain,
toChain: route.toChain,
})
}
if (
route.toChain === KnownChainId.Ethereum.Mainnet ||
route.toChain === KnownChainId.Ethereum.Sepolia ||
route.toChain === KnownChainId.Ethereum.BSC ||
route.toChain === KnownChainId.Ethereum.BSCTest
) {
return bridgeFromStacks_toEthereum({
...info,
fromChain: route.fromChain,
toChain: route.toChain,
})
}
checkNever(route)
} else {
checkNever(route)
}
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
async function bridgeFromStacks_toBitcoin(
info: Omit<BridgeFromStacksInput, "fromChain" | "toChain"> & {
fromChain:
| typeof KnownChainId.Stacks.Mainnet
| typeof KnownChainId.Stacks.Testnet
toChain:
| typeof KnownChainId.Bitcoin.Mainnet
| typeof KnownChainId.Bitcoin.Testnet
},
): Promise<BridgeFromStacksOutput> {
const contractCallInfo = getContractCallInfo(info.fromChain)
if (!contractCallInfo) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
const { network: stacksNetwork, deployerAddress } = contractCallInfo
const bitcoinNetwork =
info.toChain === KnownChainId.Bitcoin.Mainnet
? btc.NETWORK
: btc.TEST_NETWORK
const options = composeTxXLINK(
"btc-bridge-endpoint-v1-11",
"request-peg-out-0",
{
"peg-out-address": addressToScriptPubKey(bitcoinNetwork, info.toAddress),
amount: numberToStacksContractNumber(info.amount),
},
{ deployerAddress },
)
const { transactionHex } = await info.signTransaction(options)
const broadcastResponse = await broadcastTransaction(
deserializeTransaction(transactionHex),
stacksNetwork,
)
if (broadcastResponse.error) {
throw new StacksTransactionBroadcastError(broadcastResponse)
}
return { txid: broadcastResponse.txid }
}
async function bridgeFromStacks_toEthereum(
info: Omit<BridgeFromStacksInput, "fromChain" | "toChain"> & {
fromChain:
| typeof KnownChainId.Stacks.Mainnet
| typeof KnownChainId.Stacks.Testnet
toChain:
| typeof KnownChainId.Ethereum.Mainnet
| typeof KnownChainId.Ethereum.Sepolia
| typeof KnownChainId.Ethereum.BSC
| typeof KnownChainId.Ethereum.BSCTest
},
): Promise<BridgeFromStacksOutput> {
const contractCallInfo = getContractCallInfo(info.fromChain)
const tokenContractInfo = getTokenContractInfo(info.fromChain, info.fromToken)
if (contractCallInfo == null || tokenContractInfo == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
const options = composeTxXLINK(
"cross-bridge-endpoint-v1-03",
"transfer-to-unwrap",
{
"token-trait": `${tokenContractInfo.deployerAddress}.${tokenContractInfo.contractName}`,
"amount-in-fixed": numberToStacksContractNumber(info.amount),
"the-chain-id": contractAssignedChainIdFromBridgeChain(info.toChain),
"settle-address": decodeHex(info.toAddress),
},
{ deployerAddress: contractCallInfo.deployerAddress },
)
const { transactionHex } = await info.signTransaction(options)
const broadcastResponse = await broadcastTransaction(
deserializeTransaction(transactionHex),
contractCallInfo.network,
)
if (broadcastResponse.error) {
throw new StacksTransactionBroadcastError(broadcastResponse)
}
return { txid: broadcastResponse.txid }
}

View File

@@ -0,0 +1,136 @@
import {
executeReadonlyCallXLINK,
getContractCallInfo,
numberFromStacksContractNumber,
} from "../stacksUtils/xlinkContractHelpers"
import { BigNumber } from "../utils/BigNumber"
import { GetSupportedRoutesFnAnyResult } from "../utils/buildSupportedRoutes"
import { UnsupportedBridgeRouteError } from "../utils/errors"
import { checkNever } from "../utils/typeHelpers"
import {
KnownChainId,
KnownTokenId,
TokenIdInternal,
} from "../utils/types.internal"
import { supportedRoutes } from "./bridgeFromBitcoin"
import { ChainId, TokenId } from "./types"
export interface BridgeFeeFromBitcoinInput {
fromChain: ChainId
toChain: ChainId
amount: string
}
export interface BridgeFeeFromBitcoinOutput {
paused: boolean
feeToken: TokenId
feeRate: string
minFeeAmount: string
minBridgeAmount: null | string
maxBridgeAmount: null | string
}
export const bridgeFeeFromBitcoin = async (
info: BridgeFeeFromBitcoinInput,
): Promise<BridgeFeeFromBitcoinOutput> => {
const res: GetSupportedRoutesFnAnyResult =
await supportedRoutes.getSupportedTokens(info.fromChain, info.toChain)
if (res.length <= 0) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
KnownTokenId.Bitcoin.BTC,
)
}
const route = await supportedRoutes.pickLeftToRightRouteOrThrow(
info.fromChain,
info.toChain,
KnownTokenId.Bitcoin.BTC,
res[0].toToken,
)
if (
route.fromChain === KnownChainId.Bitcoin.Mainnet ||
route.fromChain === KnownChainId.Bitcoin.Testnet
) {
if (
route.toChain === KnownChainId.Stacks.Mainnet ||
route.toChain === KnownChainId.Stacks.Testnet
) {
return bridgeFeeFromBitcoin_toStacks({
...info,
fromChain: route.fromChain,
toChain: route.toChain,
})
}
checkNever(route)
} else {
checkNever(route)
}
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
KnownTokenId.Bitcoin.BTC,
res[0].toToken,
)
}
async function bridgeFeeFromBitcoin_toStacks(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info: Omit<BridgeFeeFromBitcoinInput, "fromChain" | "toChain"> & {
fromChain:
| typeof KnownChainId.Bitcoin.Mainnet
| typeof KnownChainId.Bitcoin.Testnet
toChain:
| typeof KnownChainId.Stacks.Mainnet
| typeof KnownChainId.Stacks.Testnet
},
): Promise<BridgeFeeFromBitcoinOutput> {
const contractCallInfo = getContractCallInfo(info.toChain)
if (contractCallInfo == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
KnownTokenId.Bitcoin.BTC,
)
}
const [paused, pegInFeeRate, pegInMinFee] = await Promise.all([
executeReadonlyCallXLINK(
"btc-bridge-endpoint-v1-11",
"is-peg-in-paused",
{},
{
deployerAddress: contractCallInfo.deployerAddress,
},
),
executeReadonlyCallXLINK(
"btc-bridge-endpoint-v1-11",
"get-peg-in-fee",
{},
{
deployerAddress: contractCallInfo.deployerAddress,
},
).then(numberFromStacksContractNumber),
executeReadonlyCallXLINK(
"btc-bridge-endpoint-v1-11",
"get-peg-in-min-fee",
{},
{
deployerAddress: contractCallInfo.deployerAddress,
},
).then(numberFromStacksContractNumber),
])
return {
paused,
feeToken: TokenIdInternal.toTokenId(KnownTokenId.Bitcoin.BTC),
feeRate: BigNumber.toString(pegInFeeRate),
minFeeAmount: BigNumber.toString(pegInMinFee),
minBridgeAmount: BigNumber.toString(pegInMinFee),
maxBridgeAmount: null,
}
}

View File

@@ -0,0 +1,165 @@
import { readContract } from "viem/actions"
import { bridgeEndpointAbi } from "../ethereumUtils/contractAbi/bridgeEndpoint"
import { ethEndpointContractAddresses } from "../ethereumUtils/ethContractAddresses"
import {
getContractCallInfo,
getTokenContractInfo,
numberFromSolidityContractNumber,
} from "../ethereumUtils/xlinkContractHelpers"
import { BigNumber } from "../utils/BigNumber"
import { UnsupportedBridgeRouteError } from "../utils/errors"
import { checkNever } from "../utils/typeHelpers"
import { KnownChainId } from "../utils/types.internal"
import { supportedRoutes } from "./bridgeFromEthereum"
import { ChainId, TokenId } from "./types"
export interface BridgeInfoFromEthereumInput {
fromChain: ChainId
toChain: ChainId
fromToken: TokenId
toToken: TokenId
amount: string
}
export interface BridgeInfoFromEthereumOutput {
paused: boolean
feeToken: TokenId
feeRate: string
minFeeAmount: string
minBridgeAmount: null | string
maxBridgeAmount: null | string
}
export async function bridgeInfoFromEthereum(
info: BridgeInfoFromEthereumInput,
): Promise<BridgeInfoFromEthereumOutput> {
const route = await supportedRoutes.pickLeftToRightRouteOrThrow(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
if (
route.fromChain === KnownChainId.Ethereum.Mainnet ||
route.fromChain === KnownChainId.Ethereum.BSC ||
route.fromChain === KnownChainId.Ethereum.Sepolia ||
route.fromChain === KnownChainId.Ethereum.BSCTest
) {
if (
route.toChain === KnownChainId.Stacks.Mainnet ||
route.toChain === KnownChainId.Stacks.Testnet
) {
return bridgeInfoFromEthereum_toStacks({
...info,
fromChain: route.fromChain,
toChain: route.toChain,
})
}
// if (KnownChainId.isBitcoinChain(route.toChain)) {
// return bridgeInfoFromEthereum_toBitcoin({
// ...info,
// fromChain: route.fromChain,
// toChain: route.toChain,
// })
// }
checkNever(route)
} else {
checkNever(route)
}
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
async function bridgeInfoFromEthereum_toStacks(
info: Omit<BridgeInfoFromEthereumInput, "fromChain" | "toChain"> & {
fromChain: KnownChainId.EthereumChain
toChain: KnownChainId.StacksChain
},
): Promise<BridgeInfoFromEthereumOutput> {
const bridgeEndpointAddress =
ethEndpointContractAddresses.bridgeEndpoint[info.fromChain]
const contractCallInfo = getContractCallInfo(info.fromChain)
const fromTokenContractAddress = getTokenContractInfo(
info.fromChain,
info.fromToken,
)
if (contractCallInfo == null || fromTokenContractAddress == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
const [
paused,
feePctPerToken,
minFeePerToken,
minAmountPerToken,
maxAmountPerToken,
] = await Promise.all([
readContract(contractCallInfo.client, {
abi: bridgeEndpointAbi,
address: bridgeEndpointAddress,
functionName: "paused",
args: [],
}),
readContract(contractCallInfo.client, {
abi: bridgeEndpointAbi,
address: bridgeEndpointAddress,
functionName: "feePctPerToken",
args: [fromTokenContractAddress.contractAddress],
}).then(numberFromSolidityContractNumber),
readContract(contractCallInfo.client, {
abi: bridgeEndpointAbi,
address: bridgeEndpointAddress,
functionName: "feePctPerToken",
args: [fromTokenContractAddress.contractAddress],
}).then(numberFromSolidityContractNumber),
readContract(contractCallInfo.client, {
abi: bridgeEndpointAbi,
address: bridgeEndpointAddress,
functionName: "minFeePerToken",
args: [fromTokenContractAddress.contractAddress],
}).then(numberFromSolidityContractNumber),
readContract(contractCallInfo.client, {
abi: bridgeEndpointAbi,
address: bridgeEndpointAddress,
functionName: "minAmountPerToken",
args: [fromTokenContractAddress.contractAddress],
}).then(numberFromSolidityContractNumber),
readContract(contractCallInfo.client, {
abi: bridgeEndpointAbi,
address: bridgeEndpointAddress,
functionName: "maxAmountPerToken",
args: [fromTokenContractAddress.contractAddress],
}).then(numberFromSolidityContractNumber),
])
const finalMinBridgeAmount = BigNumber.max([
minAmountPerToken,
minFeePerToken,
])
return {
paused,
feeToken: info.fromToken,
feeRate: BigNumber.toString(feePctPerToken),
minFeeAmount: BigNumber.toString(minFeePerToken),
minBridgeAmount: BigNumber.isZero(finalMinBridgeAmount)
? null
: BigNumber.toString(finalMinBridgeAmount),
maxBridgeAmount: BigNumber.isZero(maxAmountPerToken)
? null
: BigNumber.toString(maxAmountPerToken),
}
}

View File

@@ -0,0 +1,258 @@
import { contractAssignedChainIdFromBridgeChain } from "../ethereumUtils/crossContractDataMapping"
import {
executeReadonlyCallXLINK,
getContractCallInfo,
getTokenContractInfo,
numberFromStacksContractNumber,
} from "../stacksUtils/xlinkContractHelpers"
import { BigNumber } from "../utils/BigNumber"
import { UnsupportedBridgeRouteError } from "../utils/errors"
import { checkNever } from "../utils/typeHelpers"
import { KnownChainId, TokenIdInternal } from "../utils/types.internal"
import { supportedRoutes } from "./bridgeFromStacks"
import { ChainId, TokenId } from "./types"
export interface BridgeInfoFromStacksInput {
fromChain: ChainId
toChain: ChainId
fromToken: TokenId
toToken: TokenId
amount: string
}
export interface BridgeInfoFromStacksOutput {
paused: boolean
feeToken: TokenId
feeRate: string
minFeeAmount: string
minBridgeAmount: null | string
maxBridgeAmount: null | string
}
export async function bridgeInfoFromStacks(
info: BridgeInfoFromStacksInput,
): Promise<BridgeInfoFromStacksOutput> {
const route = await supportedRoutes.pickLeftToRightRouteOrThrow(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
if (
route.fromChain === KnownChainId.Stacks.Mainnet ||
route.fromChain === KnownChainId.Stacks.Testnet
) {
if (
route.toChain === KnownChainId.Bitcoin.Mainnet ||
route.toChain === KnownChainId.Bitcoin.Testnet
) {
return bridgeInfoFromStacks_toBitcoin({
...info,
fromChain: route.fromChain,
toChain: route.toChain,
})
}
if (
route.toChain === KnownChainId.Ethereum.Mainnet ||
route.toChain === KnownChainId.Ethereum.Sepolia ||
route.toChain === KnownChainId.Ethereum.BSC ||
route.toChain === KnownChainId.Ethereum.BSCTest
) {
return bridgeInfoFromStacks_toEthereum({
...info,
fromChain: route.fromChain,
toChain: route.toChain,
})
}
checkNever(route)
} else {
checkNever(route)
}
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
async function bridgeInfoFromStacks_toBitcoin(
info: Omit<BridgeInfoFromStacksInput, "fromChain" | "toChain"> & {
fromChain:
| typeof KnownChainId.Stacks.Mainnet
| typeof KnownChainId.Stacks.Testnet
toChain:
| typeof KnownChainId.Bitcoin.Mainnet
| typeof KnownChainId.Bitcoin.Testnet
},
): Promise<BridgeInfoFromStacksOutput> {
const contractCallInfo = getContractCallInfo(info.fromChain)
if (!contractCallInfo) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
const [pegOutFeeRate, pegOutMinFee, paused] = await Promise.all([
executeReadonlyCallXLINK(
"btc-bridge-endpoint-v1-11",
"get-peg-out-fee",
{},
{
deployerAddress: contractCallInfo.deployerAddress,
},
).then(numberFromStacksContractNumber),
executeReadonlyCallXLINK(
"btc-bridge-endpoint-v1-11",
"get-peg-out-min-fee",
{},
{
deployerAddress: contractCallInfo.deployerAddress,
},
).then(numberFromStacksContractNumber),
executeReadonlyCallXLINK(
"btc-bridge-endpoint-v1-11",
"is-peg-out-paused",
{},
{
deployerAddress: contractCallInfo.deployerAddress,
},
),
])
return {
paused,
feeToken: TokenIdInternal.toTokenId(info.fromToken),
feeRate: BigNumber.toString(pegOutFeeRate),
minFeeAmount: BigNumber.toString(pegOutMinFee),
minBridgeAmount: BigNumber.toString(pegOutMinFee),
maxBridgeAmount: null,
}
}
async function bridgeInfoFromStacks_toEthereum(
info: Omit<BridgeInfoFromStacksInput, "fromChain" | "toChain"> & {
fromChain:
| typeof KnownChainId.Stacks.Mainnet
| typeof KnownChainId.Stacks.Testnet
toChain:
| typeof KnownChainId.Ethereum.Mainnet
| typeof KnownChainId.Ethereum.Sepolia
| typeof KnownChainId.Ethereum.BSC
| typeof KnownChainId.Ethereum.BSCTest
},
): Promise<BridgeInfoFromStacksOutput> {
const contractCallInfo = getContractCallInfo(info.fromChain)
const tokenContractInfo = getTokenContractInfo(info.fromChain, info.fromToken)
if (contractCallInfo == null || tokenContractInfo == null) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
const [tokenIdResp, approvedTokenResp, paused] = await Promise.all([
executeReadonlyCallXLINK(
"cross-bridge-endpoint-v1-03",
"get-approved-token-id-or-fail",
{
token: `${tokenContractInfo.deployerAddress}.${tokenContractInfo.contractName}`,
},
{
deployerAddress: contractCallInfo.deployerAddress,
},
),
executeReadonlyCallXLINK(
"cross-bridge-endpoint-v1-03",
"get-approved-token-or-fail",
{
token: `${tokenContractInfo.deployerAddress}.${tokenContractInfo.contractName}`,
},
{
deployerAddress: contractCallInfo.deployerAddress,
},
),
executeReadonlyCallXLINK(
"cross-bridge-endpoint-v1-03",
"get-paused",
{},
{
deployerAddress: contractCallInfo.deployerAddress,
},
),
])
if (
tokenIdResp.type === "error" ||
approvedTokenResp.type === "error" ||
!approvedTokenResp.value.approved
) {
throw new UnsupportedBridgeRouteError(
info.fromChain,
info.toChain,
info.fromToken,
info.toToken,
)
}
const [minFee, reserve] = await Promise.all([
executeReadonlyCallXLINK(
"cross-bridge-endpoint-v1-03",
"get-min-fee-or-default",
{
"the-chain-id": contractAssignedChainIdFromBridgeChain(info.toChain),
"the-token-id": tokenIdResp.value,
},
{
deployerAddress: contractCallInfo.deployerAddress,
},
).then(numberFromStacksContractNumber),
executeReadonlyCallXLINK(
"cross-bridge-endpoint-v1-03",
"get-token-reserve-or-default",
{
"the-chain-id": contractAssignedChainIdFromBridgeChain(info.toChain),
"the-token-id": tokenIdResp.value,
},
{
deployerAddress: contractCallInfo.deployerAddress,
},
).then(numberFromStacksContractNumber),
])
const contractSetMinBridgeAmount = numberFromStacksContractNumber(
approvedTokenResp.value["min-amount"],
)
const contractSetMaxBridgeAmount = numberFromStacksContractNumber(
approvedTokenResp.value["max-amount"],
)
const finalMinBridgeAmount = BigNumber.max([
contractSetMinBridgeAmount,
minFee,
])
return {
paused,
feeToken: TokenIdInternal.toTokenId(info.fromToken),
feeRate: BigNumber.toString(approvedTokenResp.value.fee),
minFeeAmount: BigNumber.toString(minFee),
minBridgeAmount: BigNumber.isZero(finalMinBridgeAmount)
? null
: BigNumber.toString(finalMinBridgeAmount),
maxBridgeAmount: BigNumber.isZero(contractSetMaxBridgeAmount)
? BigNumber.toString(reserve)
: BigNumber.toString(
BigNumber.min([reserve, contractSetMaxBridgeAmount]),
),
}
}

View File

@@ -0,0 +1,12 @@
export type TokenId = string & { __brand: "XLINK SDK Token Id" }
export type ChainId = string & { __brand: "XLINK SDK Chain Id" }
export interface SupportedToken {
fromChain: ChainId
fromToken: TokenId
toChain: ChainId
toToken: TokenId
}
export type TokenAmount = string & { __brand: "XLINK SDK Token Amount" }

5
tsconfig.json Normal file
View File

@@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "@c4605/toolconfs/tsconfig-esModule",
"exclude": ["lib"]
}

7
vitest.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
// ... Specify options here.
},
})