mirror of
https://github.com/Brotocol-xyz/bro-sdk.git
synced 2026-01-12 06:44:18 +08:00
initial commit
This commit is contained in:
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
lib
|
||||||
|
generated/smartContract
|
||||||
11
.eslintrc.cjs
Normal file
11
.eslintrc.cjs
Normal 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
24
.github/workflows/test.yml
vendored
Normal 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
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
lib/
|
||||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
generated/smartContract
|
||||||
4
.prettierrc.cjs
Normal file
4
.prettierrc.cjs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
...require("@c4605/toolconfs/prettierrc"),
|
||||||
|
singleQuote: false,
|
||||||
|
}
|
||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
10
generated/smartContract/contracts_xlink.ts
Normal file
10
generated/smartContract/contracts_xlink.ts
Normal 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
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
1
generated/smartContractHelpers/codegenImport.ts
Normal file
1
generated/smartContractHelpers/codegenImport.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "clarity-codegen"
|
||||||
61
package.json
Normal file
61
package.json
Normal 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
3136
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
scripts/generateClarityTranscoders.ts
Normal file
13
scripts/generateClarityTranscoders.ts
Normal 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
81
src/XLinkSDK.ts
Normal 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
|
||||||
|
}
|
||||||
50
src/bitcoinUtils/bitcoinHelpers.ts
Normal file
50
src/bitcoinUtils/bitcoinHelpers.ts
Normal 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))
|
||||||
|
}
|
||||||
22
src/bitcoinUtils/broadcastSignedTransaction.ts
Normal file
22
src/bitcoinUtils/broadcastSignedTransaction.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/bitcoinUtils/btcAddresses.ts
Normal file
28
src/bitcoinUtils/btcAddresses.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/bitcoinUtils/createSendBitcoinTransaction.ts
Normal file
54
src/bitcoinUtils/createSendBitcoinTransaction.ts
Normal 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 }
|
||||||
|
}
|
||||||
85
src/bitcoinUtils/createSendInscriptionTransaction.ts
Normal file
85
src/bitcoinUtils/createSendInscriptionTransaction.ts
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/bitcoinUtils/createTransaction.ts
Normal file
54
src/bitcoinUtils/createTransaction.ts
Normal 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
|
||||||
|
}
|
||||||
21
src/bitcoinUtils/errors.ts
Normal file
21
src/bitcoinUtils/errors.ts
Normal 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}`
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/bitcoinUtils/mempoolFetch.ts
Normal file
83
src/bitcoinUtils/mempoolFetch.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
196
src/bitcoinUtils/prepareTransaction.ts
Normal file
196
src/bitcoinUtils/prepareTransaction.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src/bitcoinUtils/selectUTXOs.ts
Normal file
116
src/bitcoinUtils/selectUTXOs.ts
Normal 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
32
src/config.ts
Normal 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(),
|
||||||
|
})
|
||||||
142
src/ethereumUtils/contractAbi/bridgeEndpoint.ts
Normal file
142
src/ethereumUtils/contractAbi/bridgeEndpoint.ts
Normal 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
|
||||||
19
src/ethereumUtils/crossContractDataMapping.ts
Normal file
19
src/ethereumUtils/crossContractDataMapping.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/ethereumUtils/ethContractAddresses.ts
Normal file
86
src/ethereumUtils/ethContractAddresses.ts
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
89
src/ethereumUtils/xlinkContractHelpers.ts
Normal file
89
src/ethereumUtils/xlinkContractHelpers.ts
Normal 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
2
src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./xlinkSdkUtils/types"
|
||||||
|
export * from "./XLINKSDK"
|
||||||
91
src/stacksUtils/createBridgeOrder.ts
Normal file
91
src/stacksUtils/createBridgeOrder.ts
Normal 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! }
|
||||||
|
}
|
||||||
65
src/stacksUtils/stxContractAddresses.ts
Normal file
65
src/stacksUtils/stxContractAddresses.ts
Normal 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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
86
src/stacksUtils/validateBridgeOrder.ts
Normal file
86
src/stacksUtils/validateBridgeOrder.ts
Normal 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
|
||||||
|
}
|
||||||
95
src/stacksUtils/xlinkContractHelpers.ts
Normal file
95
src/stacksUtils/xlinkContractHelpers.ts
Normal 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
354
src/utils/BigNumber.ts
Normal 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
92
src/utils/arrayHelpers.ts
Normal 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
|
||||||
|
}
|
||||||
9
src/utils/bigintHelpers.ts
Normal file
9
src/utils/bigintHelpers.ts
Normal 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)
|
||||||
|
}
|
||||||
309
src/utils/buildSupportedRoutes.ts
Normal file
309
src/utils/buildSupportedRoutes.ts
Normal 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
49
src/utils/errors.ts
Normal 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
57
src/utils/hexHelpers.ts
Normal 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
146
src/utils/isPlainObject.ts
Normal 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
18
src/utils/typeHelpers.ts
Normal 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]
|
||||||
|
}
|
||||||
94
src/utils/types.internal.ts
Normal file
94
src/utils/types.internal.ts
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
243
src/xlinkSdkUtils/bridgeFromBitcoin.ts
Normal file
243
src/xlinkSdkUtils/bridgeFromBitcoin.ts
Normal 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: "" }
|
||||||
|
}
|
||||||
227
src/xlinkSdkUtils/bridgeFromEthereum.ts
Normal file
227
src/xlinkSdkUtils/bridgeFromEthereum.ts
Normal 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: "" }
|
||||||
|
}
|
||||||
272
src/xlinkSdkUtils/bridgeFromStacks.ts
Normal file
272
src/xlinkSdkUtils/bridgeFromStacks.ts
Normal 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 }
|
||||||
|
}
|
||||||
136
src/xlinkSdkUtils/bridgeInfoFromBitcoin.ts
Normal file
136
src/xlinkSdkUtils/bridgeInfoFromBitcoin.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
165
src/xlinkSdkUtils/bridgeInfoFromEthereum.ts
Normal file
165
src/xlinkSdkUtils/bridgeInfoFromEthereum.ts
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
258
src/xlinkSdkUtils/bridgeInfoFromStacks.ts
Normal file
258
src/xlinkSdkUtils/bridgeInfoFromStacks.ts
Normal 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]),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/xlinkSdkUtils/types.ts
Normal file
12
src/xlinkSdkUtils/types.ts
Normal 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
5
tsconfig.json
Normal 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
7
vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vitest/config"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
// ... Specify options here.
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user