From 24b0e5a613c8554db667c0a0ede5bb83f60d63d9 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Mon, 16 Sep 2024 12:58:42 +0100 Subject: [PATCH 1/6] fix: update dust-threshold Signed-off-by: Gregory Hill --- sdk/package-lock.json | 4 +-- sdk/package.json | 2 +- sdk/src/wallet/utxo.ts | 1 + sdk/test/utxo.test.ts | 72 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 72a5f75d..eac03dc1 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gobob/bob-sdk", - "version": "2.2.8", + "version": "2.2.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gobob/bob-sdk", - "version": "2.2.8", + "version": "2.2.9", "dependencies": { "@scure/base": "^1.1.7", "@scure/btc-signer": "^1.3.2", diff --git a/sdk/package.json b/sdk/package.json index 38aaea35..0928aaf6 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@gobob/bob-sdk", - "version": "2.2.8", + "version": "2.2.9", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { diff --git a/sdk/src/wallet/utxo.ts b/sdk/src/wallet/utxo.ts index 16ed12ef..bae179f2 100644 --- a/sdk/src/wallet/utxo.ts +++ b/sdk/src/wallet/utxo.ts @@ -116,6 +116,7 @@ export async function createBitcoinPsbt( network: getBtcNetwork(network), allowUnknownOutputs: true, // Required for OP_RETURN allowLegacyWitnessUtxo: true, // Required for P2SH-P2WPKH + dust: BigInt(546) as any, // TODO: update scure-btc-signer }); if (!transaction || !transaction.tx) { diff --git a/sdk/test/utxo.test.ts b/sdk/test/utxo.test.ts index 1af35f7c..27d31423 100644 --- a/sdk/test/utxo.test.ts +++ b/sdk/test/utxo.test.ts @@ -1,6 +1,6 @@ import { describe, it, assert } from 'vitest'; import { AddressType, getAddressInfo, Network } from 'bitcoin-address-validation'; -import { Address, NETWORK, OutScript, Script, Transaction } from '@scure/btc-signer'; +import { Address, NETWORK, OutScript, Script, Transaction, p2sh, p2wpkh, selectUTXO } from '@scure/btc-signer'; import { hex, base64 } from '@scure/base'; import { createBitcoinPsbt, getInputFromUtxoAndTx } from '../src/wallet/utxo'; import { TransactionOutput } from '@scure/btc-signer/psbt'; @@ -186,4 +186,74 @@ describe('UTXO Tests', () => { assert(input); } }); + + // custom test using partially real data that would otherwise produce an invalid output + // below the dust limit if we did not manually configure that to the correct value of 546 + it('should not output too small change', async () => { + const inputScript = Buffer.from("a9147ecd91afdcadf6f1b9e8e026a312e4cce61e63ea87", "hex"); + const outputOpReturn = Buffer.from("6a200000000000000000000000000000000000000000000000000000000000000000", "hex"); + + const publicKey = '03b366c69e8237d9be7c4f1ac2a7abc6a79932fbf3de4e2f6c04797d7ef27abfe1'; + const inner = p2wpkh(Buffer.from(publicKey, "hex"), NETWORK); + const redeemScript = p2sh(inner); + + const transaction = selectUTXO([ + { + txid: Buffer.alloc(32, 0).toString("hex"), + index: 0, + ...redeemScript, + witnessUtxo: { + script: inputScript, + amount: BigInt(23328) // 0.00023328 + }, + }, + { + txid: Buffer.alloc(32, 0).toString("hex"), + index: 0, + ...redeemScript, + witnessUtxo: { + script: inputScript, + amount: BigInt(14476) // 0.00014476 + }, + }, + { + txid: Buffer.alloc(32, 0).toString("hex"), + index: 0, + ...redeemScript, + witnessUtxo: { + script: inputScript, + amount: BigInt(4389) // 0.00004389 + }, + }, + { + txid: Buffer.alloc(32, 0).toString("hex"), + index: 0, + ...redeemScript, + witnessUtxo: { + script: inputScript, + amount: BigInt(60037) // 0.00060037 + }, + } + ], [ + { + script: outputOpReturn, + amount: BigInt(0) + }, + { + address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", + amount: BigInt(100000) // 0.001 BTC + } + ], "default", { + changeAddress: "3DFVKuT9Ft4rWpysAZ1bHpg55EBy1HVPcr", + feePerByte: BigInt(Math.ceil(4)), + bip69: true, + createTx: true, + network: NETWORK, + allowUnknownOutputs: true, + allowLegacyWitnessUtxo: true, + dust: BigInt(546) as any, + }); + + assert.isDefined(transaction); + }); }); From 7fefcb28e11c794643456c90b47c2420cc2dc716 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Mon, 16 Sep 2024 15:32:13 +0100 Subject: [PATCH 2/6] feat: add more api fields to gateway Signed-off-by: Gregory Hill --- sdk/package-lock.json | 4 ++-- sdk/package.json | 2 +- sdk/src/gateway/client.ts | 24 +++++++++++++------- sdk/src/gateway/types.ts | 17 +++++++++++++-- sdk/test/gateway.test.ts | 46 ++++++++++++++++++++++++++++++++++----- 5 files changed, 74 insertions(+), 19 deletions(-) diff --git a/sdk/package-lock.json b/sdk/package-lock.json index eac03dc1..fab29bfd 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gobob/bob-sdk", - "version": "2.2.9", + "version": "2.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gobob/bob-sdk", - "version": "2.2.9", + "version": "2.3.0", "dependencies": { "@scure/base": "^1.1.7", "@scure/btc-signer": "^1.3.2", diff --git a/sdk/package.json b/sdk/package.json index 0928aaf6..514d464a 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@gobob/bob-sdk", - "version": "2.2.9", + "version": "2.3.0", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { diff --git a/sdk/src/gateway/client.ts b/sdk/src/gateway/client.ts index ab619559..eca8f41d 100644 --- a/sdk/src/gateway/client.ts +++ b/sdk/src/gateway/client.ts @@ -13,6 +13,7 @@ import { GatewayStartOrder, GatewayStrategy, EvmAddress, + GatewayTokensData, } from "./types"; import { SYMBOL_LOOKUP, ADDRESS_LOOKUP } from "./tokens"; import { createBitcoinPsbt } from "../wallet"; @@ -80,7 +81,7 @@ export class GatewayApiClient { GatewayQuoteParams, "amount" | "fromChain" | "fromToken" | "fromUserAddress" | "toUserAddress" >, - ): Promise { + ): Promise { const isMainnet = params.toChain === ChainId.BOB || (typeof params.toChain === "string" && params.toChain.toLowerCase() === Chain.BOB); @@ -97,7 +98,7 @@ export class GatewayApiClient { throw new Error("Invalid output chain"); } - let outputToken = ""; + let outputTokenAddress = ""; let strategyAddress: string | undefined; const toToken = params.toToken.toLowerCase(); @@ -106,16 +107,16 @@ export class GatewayApiClient { } if (toToken.startsWith("0x")) { - outputToken = toToken; + outputTokenAddress = toToken; } else if (isMainnet && this.chain === Chain.BOB && SYMBOL_LOOKUP[ChainId.BOB][toToken]) { - outputToken = SYMBOL_LOOKUP[ChainId.BOB][toToken].address; + outputTokenAddress = SYMBOL_LOOKUP[ChainId.BOB][toToken].address; } else if (isTestnet && this.chain === Chain.BOB_SEPOLIA && SYMBOL_LOOKUP[ChainId.BOB_SEPOLIA][toToken]) { - outputToken = SYMBOL_LOOKUP[ChainId.BOB_SEPOLIA][toToken].address; + outputTokenAddress = SYMBOL_LOOKUP[ChainId.BOB_SEPOLIA][toToken].address; } else { throw new Error("Unknown output token"); } - var url = new URL(`${this.baseUrl}/quote/${outputToken}`); + var url = new URL(`${this.baseUrl}/quote/${outputTokenAddress}`); if (strategyAddress) { url.searchParams.append("strategy", `${strategyAddress}`); } @@ -135,6 +136,8 @@ export class GatewayApiClient { return { ...quote, fee: quote.fee + (params.gasRefill || 0), + baseToken: ADDRESS_LOOKUP[quote.baseTokenAddress], + outputToken: ADDRESS_LOOKUP[outputTokenAddress], }; } @@ -252,11 +255,16 @@ export class GatewayApiClient { * @param userAddress The user's EVM address. * @returns {Promise} The array of account orders. */ - async getOrders(userAddress: EvmAddress): Promise { + async getOrders(userAddress: EvmAddress): Promise<(GatewayOrder & Optional)[]> { const response = await this.fetchGet(`${this.baseUrl}/orders/${userAddress}`); const orders: GatewayOrderResponse[] = await response.json(); return orders.map((order) => { - return { gasRefill: order.satsToConvertToEth, ...order }; + return { + gasRefill: order.satsToConvertToEth, + ...order, + baseToken: ADDRESS_LOOKUP[order.baseTokenAddress], + outputToken: ADDRESS_LOOKUP[order.outputTokenAddress] + }; }); } diff --git a/sdk/src/gateway/types.ts b/sdk/src/gateway/types.ts index 8480dce1..f527746c 100644 --- a/sdk/src/gateway/types.ts +++ b/sdk/src/gateway/types.ts @@ -160,6 +160,8 @@ export interface GatewayStrategyContract { export type GatewayQuote = { /** @description The gateway address */ gatewayAddress: EvmAddress; + /** @description The base token address (e.g. wBTC or tBTC) */ + baseTokenAddress: EvmAddress; /** @description The minimum amount of Bitcoin to send */ dustThreshold: number; /** @description The satoshi output amount */ @@ -188,8 +190,8 @@ export type GatewayCreateOrderRequest = { export type GatewayOrderResponse = { /** @description The gateway address */ gatewayAddress: EvmAddress; - /** @description The token address */ - tokenAddress: EvmAddress; + /** @description The base token address (e.g. wBTC or tBTC) */ + baseTokenAddress: EvmAddress; /** @description The Bitcoin txid */ txid: string; /** @description True when the order was executed on BOB */ @@ -208,6 +210,12 @@ export type GatewayOrderResponse = { strategyAddress?: EvmAddress; /** @description The gas refill in satoshis */ satsToConvertToEth: number; + /** @description The amount of ETH received */ + outputEthAmount?: number; + /** @description The output token (from strategies) */ + outputTokenAddress?: EvmAddress; + /** @description The output amount (from strategies) */ + outputTokenAmount?: number; }; /** Order given by the Gateway API once the bitcoin tx is submitted */ @@ -219,6 +227,11 @@ export type GatewayOrder = Omit< "satsToConvertToEth" >; +export type GatewayTokensData = { + baseToken: Token, + outputToken: Token, +}; + /** @dev Internal */ export type GatewayCreateOrderResponse = { uuid: string; diff --git a/sdk/test/gateway.test.ts b/sdk/test/gateway.test.ts index fa484f79..c0ad1384 100644 --- a/sdk/test/gateway.test.ts +++ b/sdk/test/gateway.test.ts @@ -7,6 +7,11 @@ import { ZeroAddress } from "ethers"; import nock from "nock"; import * as bitcoin from "bitcoinjs-lib"; +const TBTC = SYMBOL_LOOKUP[ChainId.BOB]["tbtc"]; +const TBTC_ADDRESS = TBTC.address; +const SOLVBTC = SYMBOL_LOOKUP[ChainId.BOB]["solvbtc"]; +const SOLVBTC_ADDRESS = SOLVBTC.address; + describe("Gateway Tests", () => { it("should get chains", async () => { const gatewaySDK = new GatewaySDK("bob"); @@ -24,16 +29,19 @@ describe("Gateway Tests", () => { const mockQuote = { gatewayAddress: ZeroAddress, + baseTokenAddress: TBTC_ADDRESS, dustThreshold: 1000, satoshis: 1000, fee: 10, bitcoinAddress: "", txProofDifficultyFactor: 3, strategyAddress: ZeroAddress, + baseToken: TBTC, + outputToken: TBTC, }; nock(`${MAINNET_GATEWAY_BASE_URL}`) - .get(`/quote/${SYMBOL_LOOKUP[ChainId.BOB]["tbtc"].address}?satoshis=1000`) + .get(`/quote/${TBTC_ADDRESS}?satoshis=1000`) .times(5) .reply(200, mockQuote); @@ -57,7 +65,7 @@ describe("Gateway Tests", () => { }), mockQuote); assert.deepEqual(await gatewaySDK.getQuote({ toChain: "BOB", - toToken: SYMBOL_LOOKUP[ChainId.BOB]["tbtc"].address, + toToken: TBTC_ADDRESS, toUserAddress: ZeroAddress, amount: 1000, }), mockQuote); @@ -71,11 +79,11 @@ describe("Gateway Tests", () => { // get the total available without amount nock(`${MAINNET_GATEWAY_BASE_URL}`) - .get(`/quote/${SYMBOL_LOOKUP[ChainId.BOB]["tbtc"].address}`) + .get(`/quote/${TBTC_ADDRESS}`) .reply(200, mockQuote); assert.deepEqual(await gatewaySDK.getQuote({ toChain: "BOB", - toToken: SYMBOL_LOOKUP[ChainId.BOB]["tbtc"].address, + toToken: TBTC_ADDRESS, toUserAddress: ZeroAddress, }), mockQuote); }); @@ -118,6 +126,7 @@ describe("Gateway Tests", () => { const mockQuote = { gatewayAddress: ZeroAddress, + baseTokenAddress: TBTC_ADDRESS, dustThreshold: 1000, satoshis: 1000, fee: 10, @@ -171,12 +180,12 @@ describe("Gateway Tests", () => { .get(`/strategies`) .reply(200, [{ strategyAddress: ZeroAddress, - inputTokenAddress: SYMBOL_LOOKUP[ChainId.BOB]["tbtc"].address, + inputTokenAddress: TBTC_ADDRESS, strategyName: "Pell Network (tBTC)", strategyType: "staking" }]); nock(`${MAINNET_GATEWAY_BASE_URL}`) - .get(`/quote/${SYMBOL_LOOKUP[ChainId.BOB]["tbtc"].address}?satoshis=1000&strategy=${ZeroAddress}`) + .get(`/quote/${TBTC_ADDRESS}?satoshis=1000&strategy=${ZeroAddress}`) .times(4) .reply(200, { gatewayAddress: ZeroAddress, @@ -215,4 +224,29 @@ describe("Gateway Tests", () => { const gatewaySDK = new GatewaySDK("bob"); assert.deepEqual(await gatewaySDK.getTokenAddresses(false), [ZeroAddress]); }); + + it("should get orders", async () => { + nock(`${MAINNET_GATEWAY_BASE_URL}`) + .get(`/orders/${ZeroAddress}`) + .reply(200, [{ + gatewayAddress: ZeroAddress, + baseTokenAddress: TBTC_ADDRESS, + txid: "", + status: true, + timestamp: 0, + tokens: "", + satoshis: 0, + fee: 0, + txProofDifficultyFactor: 0, + strategyAddress: "", + satsToConvertToEth: 0, + outputEthAmount: 0, + outputTokenAddress: SOLVBTC_ADDRESS, + outputTokenAmount: 0, + }]); + + const gatewaySDK = new GatewaySDK("bob"); + const orders = await gatewaySDK.getOrders(ZeroAddress); + assert.lengthOf(orders, 1); + }); }); From 696e0a4b1c4b80a54e46b6bd7159e9ba7d6bc242 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Mon, 16 Sep 2024 15:56:37 +0100 Subject: [PATCH 3/6] fix: returned amounts are strings Signed-off-by: Gregory Hill --- sdk/package-lock.json | 4 ++-- sdk/package.json | 2 +- sdk/src/gateway/client.ts | 6 +++--- sdk/src/gateway/types.ts | 6 +++--- sdk/test/gateway.test.ts | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/sdk/package-lock.json b/sdk/package-lock.json index fab29bfd..c28d2cc9 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gobob/bob-sdk", - "version": "2.3.0", + "version": "2.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gobob/bob-sdk", - "version": "2.3.0", + "version": "2.3.1", "dependencies": { "@scure/base": "^1.1.7", "@scure/btc-signer": "^1.3.2", diff --git a/sdk/package.json b/sdk/package.json index 514d464a..ee8c8f6a 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@gobob/bob-sdk", - "version": "2.3.0", + "version": "2.3.1", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { diff --git a/sdk/src/gateway/client.ts b/sdk/src/gateway/client.ts index eca8f41d..babfcae3 100644 --- a/sdk/src/gateway/client.ts +++ b/sdk/src/gateway/client.ts @@ -13,7 +13,7 @@ import { GatewayStartOrder, GatewayStrategy, EvmAddress, - GatewayTokensData, + GatewayTokensInfo, } from "./types"; import { SYMBOL_LOOKUP, ADDRESS_LOOKUP } from "./tokens"; import { createBitcoinPsbt } from "../wallet"; @@ -81,7 +81,7 @@ export class GatewayApiClient { GatewayQuoteParams, "amount" | "fromChain" | "fromToken" | "fromUserAddress" | "toUserAddress" >, - ): Promise { + ): Promise { const isMainnet = params.toChain === ChainId.BOB || (typeof params.toChain === "string" && params.toChain.toLowerCase() === Chain.BOB); @@ -255,7 +255,7 @@ export class GatewayApiClient { * @param userAddress The user's EVM address. * @returns {Promise} The array of account orders. */ - async getOrders(userAddress: EvmAddress): Promise<(GatewayOrder & Optional)[]> { + async getOrders(userAddress: EvmAddress): Promise<(GatewayOrder & Optional)[]> { const response = await this.fetchGet(`${this.baseUrl}/orders/${userAddress}`); const orders: GatewayOrderResponse[] = await response.json(); return orders.map((order) => { diff --git a/sdk/src/gateway/types.ts b/sdk/src/gateway/types.ts index f527746c..2f806f18 100644 --- a/sdk/src/gateway/types.ts +++ b/sdk/src/gateway/types.ts @@ -211,11 +211,11 @@ export type GatewayOrderResponse = { /** @description The gas refill in satoshis */ satsToConvertToEth: number; /** @description The amount of ETH received */ - outputEthAmount?: number; + outputEthAmount?: string; /** @description The output token (from strategies) */ outputTokenAddress?: EvmAddress; /** @description The output amount (from strategies) */ - outputTokenAmount?: number; + outputTokenAmount?: string; }; /** Order given by the Gateway API once the bitcoin tx is submitted */ @@ -227,7 +227,7 @@ export type GatewayOrder = Omit< "satsToConvertToEth" >; -export type GatewayTokensData = { +export type GatewayTokensInfo = { baseToken: Token, outputToken: Token, }; diff --git a/sdk/test/gateway.test.ts b/sdk/test/gateway.test.ts index c0ad1384..b8696234 100644 --- a/sdk/test/gateway.test.ts +++ b/sdk/test/gateway.test.ts @@ -240,9 +240,9 @@ describe("Gateway Tests", () => { txProofDifficultyFactor: 0, strategyAddress: "", satsToConvertToEth: 0, - outputEthAmount: 0, + outputEthAmount: "0", outputTokenAddress: SOLVBTC_ADDRESS, - outputTokenAmount: 0, + outputTokenAmount: "0", }]); const gatewaySDK = new GatewaySDK("bob"); From 07b9a29704e570ee4ca89a9d0cd3c9068a475ff6 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Mon, 16 Sep 2024 18:51:18 +0100 Subject: [PATCH 4/6] refactor: order fns to compute token received Signed-off-by: Gregory Hill --- sdk/package-lock.json | 4 +- sdk/package.json | 2 +- sdk/src/esplora.ts | 21 ++++++++--- sdk/src/gateway/client.ts | 32 +++++++++++++++- sdk/src/gateway/types.ts | 12 +++++- sdk/test/gateway.test.ts | 79 ++++++++++++++++++++++++++++++--------- 6 files changed, 122 insertions(+), 28 deletions(-) diff --git a/sdk/package-lock.json b/sdk/package-lock.json index c28d2cc9..02821532 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gobob/bob-sdk", - "version": "2.3.1", + "version": "2.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gobob/bob-sdk", - "version": "2.3.1", + "version": "2.3.2", "dependencies": { "@scure/base": "^1.1.7", "@scure/btc-signer": "^1.3.2", diff --git a/sdk/package.json b/sdk/package.json index ee8c8f6a..71500a0c 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@gobob/bob-sdk", - "version": "2.3.1", + "version": "2.3.2", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { diff --git a/sdk/src/esplora.ts b/sdk/src/esplora.ts index 936b31fa..7c715451 100644 --- a/sdk/src/esplora.ts +++ b/sdk/src/esplora.ts @@ -69,12 +69,17 @@ export interface Transaction { scriptpubkey_address?: string value: number }> - status: { - confirmed: boolean - block_height?: number - block_hash?: string - block_time?: number - } + status: TransactionStatus +} + +/** + * @ignore + */ +export interface TransactionStatus { + confirmed: boolean + block_height?: number + block_hash?: string + block_time?: number } /** @@ -259,6 +264,10 @@ export class EsploraClient { return this.getJson(`${this.basePath}/tx/${txId}`); } + async getTransactionStatus(txId: string): Promise { + return this.getJson(`${this.basePath}/tx/${txId}/status`); + } + /** * Get the transaction data, represented as a hex string, for a Bitcoin transaction with a given ID (txId). * diff --git a/sdk/src/gateway/client.ts b/sdk/src/gateway/client.ts index babfcae3..0072bc0a 100644 --- a/sdk/src/gateway/client.ts +++ b/sdk/src/gateway/client.ts @@ -259,11 +259,41 @@ export class GatewayApiClient { const response = await this.fetchGet(`${this.baseUrl}/orders/${userAddress}`); const orders: GatewayOrderResponse[] = await response.json(); return orders.map((order) => { + function getFinal(base?: L, output?: R) { + return order.status + ? order.strategyAddress + ? output + ? output // success + : base // failed + : base // success + : order.strategyAddress // pending + ? output + : base; + } + const getTokenAddress = (): string | undefined => { + return getFinal(order.baseTokenAddress, order.outputTokenAddress); + } return { gasRefill: order.satsToConvertToEth, ...order, baseToken: ADDRESS_LOOKUP[order.baseTokenAddress], - outputToken: ADDRESS_LOOKUP[order.outputTokenAddress] + outputToken: ADDRESS_LOOKUP[order.outputTokenAddress], + getTokenAddress, + getToken() { + return ADDRESS_LOOKUP[getTokenAddress()]; + }, + getAmount(): string | number | undefined { + const baseAmount = order.satoshis - order.fee; + return getFinal(baseAmount, order.outputTokenAmount); + }, + async getConfirmations(esploraClient: EsploraClient, latestHeight?: number): Promise { + const txStatus = await esploraClient.getTransactionStatus(order.txid); + if (!latestHeight) { + latestHeight = await esploraClient.getLatestHeight(); + } + + return txStatus.confirmed ? latestHeight - txStatus.block_height! + 1 : 0; + } }; }); } diff --git a/sdk/src/gateway/types.ts b/sdk/src/gateway/types.ts index 2f806f18..8466d4c5 100644 --- a/sdk/src/gateway/types.ts +++ b/sdk/src/gateway/types.ts @@ -1,3 +1,5 @@ +import type { EsploraClient } from "../esplora"; + type ChainSlug = string | number; type TokenSymbol = string; @@ -187,7 +189,7 @@ export type GatewayCreateOrderRequest = { satoshis: number; }; -export type GatewayOrderResponse = { +export interface GatewayOrderResponse { /** @description The gateway address */ gatewayAddress: EvmAddress; /** @description The base token address (e.g. wBTC or tBTC) */ @@ -216,6 +218,14 @@ export type GatewayOrderResponse = { outputTokenAddress?: EvmAddress; /** @description The output amount (from strategies) */ outputTokenAmount?: string; + /** @description Get the actual token address received */ + getTokenAddress(): string | undefined; + /** @description Get the actual token received */ + getToken(): Token | undefined; + /** @description Get the actual amount received of the token */ + getAmount(): string | number | undefined; + /** @description Get the number of confirmations */ + getConfirmations(esploraClient: EsploraClient, latestHeight?: number): Promise; }; /** Order given by the Gateway API once the bitcoin tx is submitted */ diff --git a/sdk/test/gateway.test.ts b/sdk/test/gateway.test.ts index b8696234..c159e0ab 100644 --- a/sdk/test/gateway.test.ts +++ b/sdk/test/gateway.test.ts @@ -226,27 +226,72 @@ describe("Gateway Tests", () => { }); it("should get orders", async () => { + const mockOrder = { + gatewayAddress: ZeroAddress, + baseTokenAddress: TBTC_ADDRESS, + txid: "", + status: false, + timestamp: 0, + tokens: "0", + satoshis: 0, + fee: 0, + txProofDifficultyFactor: 0, + satsToConvertToEth: 0, + }; nock(`${MAINNET_GATEWAY_BASE_URL}`) .get(`/orders/${ZeroAddress}`) - .reply(200, [{ - gatewayAddress: ZeroAddress, - baseTokenAddress: TBTC_ADDRESS, - txid: "", - status: true, - timestamp: 0, - tokens: "", - satoshis: 0, - fee: 0, - txProofDifficultyFactor: 0, - strategyAddress: "", - satsToConvertToEth: 0, - outputEthAmount: "0", - outputTokenAddress: SOLVBTC_ADDRESS, - outputTokenAmount: "0", - }]); + .reply(200, [ + // staking - success + { + ...mockOrder, + satoshis: 1000, + fee: 0, + status: true, + strategyAddress: ZeroAddress, + outputTokenAmount: "2000", + outputTokenAddress: SOLVBTC_ADDRESS, + }, + // staking - pending + { + ...mockOrder, + satoshis: 1000, + fee: 0, + strategyAddress: ZeroAddress, + }, + // staking - failed + { + ...mockOrder, + satoshis: 1000, + fee: 0, + status: true, + strategyAddress: ZeroAddress, + }, + // swapping - pending + { + ...mockOrder, + satoshis: 1000, + fee: 0, + }, + // swapping - success + { + ...mockOrder, + satoshis: 1000, + fee: 0, + status: true + }, + ]); const gatewaySDK = new GatewaySDK("bob"); const orders = await gatewaySDK.getOrders(ZeroAddress); - assert.lengthOf(orders, 1); + assert.lengthOf(orders, 5); + + assert.strictEqual(orders[0].getAmount(), "2000"); + assert.strictEqual(orders[1].getAmount(), undefined); + assert.strictEqual(orders[2].getAmount(), 1000); + assert.strictEqual(orders[3].getAmount(), 1000); + assert.strictEqual(orders[4].getAmount(), 1000); + + assert.strictEqual(orders[0].getToken()!.address, SOLVBTC_ADDRESS); + assert.strictEqual(orders[1].getToken(), undefined); }); }); From 88ab31c72e86ca4613201563074a7a19da410f6a Mon Sep 17 00:00:00 2001 From: Derrek <80121818+derrekcoleman@users.noreply.github.com> Date: Tue, 17 Sep 2024 09:05:27 -0600 Subject: [PATCH 5/6] Remove bot crawling protection from email addresses --- docs/docs/learn/guides/bob-gateway/index.md | 2 +- docs/docs/learn/introduction/contribution.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/learn/guides/bob-gateway/index.md b/docs/docs/learn/guides/bob-gateway/index.md index 5a423033..e97b9041 100644 --- a/docs/docs/learn/guides/bob-gateway/index.md +++ b/docs/docs/learn/guides/bob-gateway/index.md @@ -12,7 +12,7 @@ sidebar_label: BOB Gateway It's built on a trustless, RFQ-based cross-chain [swap protocol](../../../build/examples/btc-swap/) that connects professional LPs with users through a seamless swapping experience. Essentially, LPs handle the complexities of bridging and staking on behalf of users in exchange for a fee. :::tip Interested in providing liquidity? -If you are interested in being an LP for the BOB Gateway bridge, please send us an email at `gateway [ at ] gobob.xyz`. +If you are interested in being an LP for the BOB Gateway bridge, please send us an email at `gateway@gobob.xyz`. ::: All you need is a Bitcoin wallet with some BTC to send and an EVM-compatible wallet to receive your Bitcoin LST/LRT or wrapped Bitcoin on BOB. We'll even send you some ETH to cover the fees of your first few transactions on BOB. diff --git a/docs/docs/learn/introduction/contribution.md b/docs/docs/learn/introduction/contribution.md index ff1a6a67..15212a45 100644 --- a/docs/docs/learn/introduction/contribution.md +++ b/docs/docs/learn/introduction/contribution.md @@ -27,6 +27,6 @@ If you are keen to build on BOB or contribute to BOB itself: ## More -- Liquidity providers: If you are interested in being an LP for the BOB Gateway bridge, please send us an email at `gateway [ at ] gobob.xyz`. +- Liquidity providers: If you are interested in being an LP for the BOB Gateway bridge, please send us an email at `gateway@gobob.xyz`. - Press kit: [BOB Press Kit](https://build-on-bitcoin.notion.site/BOB-Press-Kit-1be66c38713d480eab01000bdd164206) - Brand assets: [BOB Brand Assets](https://drive.google.com/drive/u/0/folders/1c30QDkyWgaV8xSEpCXFWJj1WQyUjSm7N) From 2cd5f894ef8ccba3afc449c53c53a578f5ba12a8 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Wed, 18 Sep 2024 09:53:15 +0100 Subject: [PATCH 6/6] feat: add order status Signed-off-by: Gregory Hill --- sdk/package.json | 2 +- sdk/src/gateway/client.ts | 37 +++++++++++++++++++++++++++---------- sdk/src/gateway/index.ts | 9 ++++++++- sdk/src/gateway/types.ts | 24 +++++++++++++++++++++++- 4 files changed, 59 insertions(+), 13 deletions(-) diff --git a/sdk/package.json b/sdk/package.json index 71500a0c..5e837578 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@gobob/bob-sdk", - "version": "2.3.2", + "version": "2.3.3", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { diff --git a/sdk/src/gateway/client.ts b/sdk/src/gateway/client.ts index 0072bc0a..c83272c8 100644 --- a/sdk/src/gateway/client.ts +++ b/sdk/src/gateway/client.ts @@ -14,6 +14,8 @@ import { GatewayStrategy, EvmAddress, GatewayTokensInfo, + OrderStatus, + OrderStatusType, } from "./types"; import { SYMBOL_LOOKUP, ADDRESS_LOOKUP } from "./tokens"; import { createBitcoinPsbt } from "../wallet"; @@ -137,7 +139,7 @@ export class GatewayApiClient { ...quote, fee: quote.fee + (params.gasRefill || 0), baseToken: ADDRESS_LOOKUP[quote.baseTokenAddress], - outputToken: ADDRESS_LOOKUP[outputTokenAddress], + outputToken: quote.strategyAddress ? ADDRESS_LOOKUP[outputTokenAddress] : undefined, }; } @@ -255,7 +257,7 @@ export class GatewayApiClient { * @param userAddress The user's EVM address. * @returns {Promise} The array of account orders. */ - async getOrders(userAddress: EvmAddress): Promise<(GatewayOrder & Optional)[]> { + async getOrders(userAddress: EvmAddress): Promise<(GatewayOrder & GatewayTokensInfo)[]> { const response = await this.fetchGet(`${this.baseUrl}/orders/${userAddress}`); const orders: GatewayOrderResponse[] = await response.json(); return orders.map((order) => { @@ -273,6 +275,13 @@ export class GatewayApiClient { const getTokenAddress = (): string | undefined => { return getFinal(order.baseTokenAddress, order.outputTokenAddress); } + const getConfirmations = async (esploraClient: EsploraClient, latestHeight?: number) => { + const txStatus = await esploraClient.getTransactionStatus(order.txid); + if (!latestHeight) { + latestHeight = await esploraClient.getLatestHeight(); + } + return txStatus.confirmed ? latestHeight - txStatus.block_height! + 1 : 0; + } return { gasRefill: order.satsToConvertToEth, ...order, @@ -286,14 +295,22 @@ export class GatewayApiClient { const baseAmount = order.satoshis - order.fee; return getFinal(baseAmount, order.outputTokenAmount); }, - async getConfirmations(esploraClient: EsploraClient, latestHeight?: number): Promise { - const txStatus = await esploraClient.getTransactionStatus(order.txid); - if (!latestHeight) { - latestHeight = await esploraClient.getLatestHeight(); - } - - return txStatus.confirmed ? latestHeight - txStatus.block_height! + 1 : 0; - } + getConfirmations, + async getStatus(esploraClient: EsploraClient, latestHeight?: number): Promise { + const confirmations = await getConfirmations(esploraClient, latestHeight); + const hasEnoughConfirmations = confirmations >= order.txProofDifficultyFactor; + const data = { + confirmations, + confirmed: hasEnoughConfirmations + }; + return order.status + ? order.strategyAddress + ? order.outputTokenAddress + ? { status: OrderStatusType.Success, data } + : { status: OrderStatusType.Failed, data } + : { status: OrderStatusType.Success, data } + : { status: OrderStatusType.Pending, data }; + }, }; }); } diff --git a/sdk/src/gateway/index.ts b/sdk/src/gateway/index.ts index cf787bfb..4edebc63 100644 --- a/sdk/src/gateway/index.ts +++ b/sdk/src/gateway/index.ts @@ -1,2 +1,9 @@ export { GatewayApiClient as GatewaySDK } from "./client"; -export { GatewayQuoteParams, GatewayQuote, GatewayOrder, GatewayStrategyContract } from "./types"; +export { + GatewayQuoteParams, + GatewayQuote, + GatewayOrder, + GatewayStrategyContract, + OrderStatusType, + OrderStatus, +} from "./types"; diff --git a/sdk/src/gateway/types.ts b/sdk/src/gateway/types.ts index 8466d4c5..bfb90875 100644 --- a/sdk/src/gateway/types.ts +++ b/sdk/src/gateway/types.ts @@ -189,6 +189,22 @@ export type GatewayCreateOrderRequest = { satoshis: number; }; +export type OrderStatusData = { + confirmations: number; + confirmed: boolean; +}; + +export enum OrderStatusType { + Success = "Success", + Failed = "Failed", + Pending = "Pending", +} + +export type OrderStatus = + | { status: OrderStatusType.Success; data: OrderStatusData } + | { status: OrderStatusType.Failed; data: OrderStatusData } + | { status: OrderStatusType.Pending; data: OrderStatusData }; + export interface GatewayOrderResponse { /** @description The gateway address */ gatewayAddress: EvmAddress; @@ -218,6 +234,8 @@ export interface GatewayOrderResponse { outputTokenAddress?: EvmAddress; /** @description The output amount (from strategies) */ outputTokenAmount?: string; + /** @description The tx hash on the EVM chain */ + txHash?: string; /** @description Get the actual token address received */ getTokenAddress(): string | undefined; /** @description Get the actual token received */ @@ -226,6 +244,8 @@ export interface GatewayOrderResponse { getAmount(): string | number | undefined; /** @description Get the number of confirmations */ getConfirmations(esploraClient: EsploraClient, latestHeight?: number): Promise; + /** @description Get the actual order status */ + getStatus(esploraClient: EsploraClient, latestHeight?: number): Promise; }; /** Order given by the Gateway API once the bitcoin tx is submitted */ @@ -238,8 +258,10 @@ export type GatewayOrder = Omit< >; export type GatewayTokensInfo = { + /** @description The base token (e.g. wBTC or tBTC) */ baseToken: Token, - outputToken: Token, + /** @description The output token (e.g. uniBTC or SolvBTC.BBN) */ + outputToken?: Token, }; /** @dev Internal */