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