Skip to content

Commit

Permalink
feat: update gateway for v3
Browse files Browse the repository at this point in the history
Signed-off-by: Gregory Hill <[email protected]>
  • Loading branch information
gregdhill committed Aug 21, 2024
1 parent 1723f48 commit fb8d901
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 89 deletions.
3 changes: 2 additions & 1 deletion sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@scure/base": "^1.1.7",
"@scure/btc-signer": "^1.3.2",
"bitcoin-address-validation": "^2.2.3",
"bitcoinjs-lib": "^6.1.6"
"bitcoinjs-lib": "^6.1.6",
"ethers": "^6.13.2"
}
}
88 changes: 0 additions & 88 deletions sdk/src/gateway.ts

This file was deleted.

194 changes: 194 additions & 0 deletions sdk/src/gateway/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { ethers, AbiCoder } from "ethers";
import { GatewayQuoteParams } from "./types";
import { TOKENS } from "./tokens";

type EvmAddress = string;

type GatewayQuote = {
gatewayAddress: EvmAddress;
dustThreshold: number;
satoshis: number;
fee: number;
gratuity: string;
bitcoinAddress: string;
txProofDifficultyFactor: number;
strategyAddress: EvmAddress | null,
};

type GatewayCreateOrderRequest = {
gatewayAddress: EvmAddress,
strategyAddress: EvmAddress | null,
satsToConvertToEth: number,
userAddress: EvmAddress,
gatewayExtraData: string | null,
strategyExtraData: string | null,
satoshis: number,
};

type GatewayOrderResponse = {
gatewayAddress: EvmAddress;
tokenAddress: EvmAddress;
txid: string;
status: boolean;
timestamp: number;
tokens: string;
satoshis: number;
fee: number;
txProofDifficultyFactor: number;
};

type GatewayCreateOrderResponse = {
uuid: string,
opReturnHash: string,
};

type GatewayStartOrderResult = GatewayCreateOrderResponse & {
bitcoinAddress: string,
satoshis: number;
};

/**
* Base url for the mainnet Gateway API.
* @default "https://gateway-api-mainnet.gobob.xyz"
*/
export const MAINNET_GATEWAY_BASE_URL = "https://gateway-api-mainnet.gobob.xyz";

/**
* Base url for the testnet Gateway API.
* @default "https://gateway-api-testnet.gobob.xyz"
*/
export const TESTNET_GATEWAY_BASE_URL = "https://gateway-api-testnet.gobob.xyz";

export class GatewayApiClient {
private baseUrl: string;

constructor(networkOrUrl: string = "mainnet") {
switch (networkOrUrl) {
case "mainnet":
this.baseUrl = MAINNET_GATEWAY_BASE_URL;
break;
case "testnet":
this.baseUrl = TESTNET_GATEWAY_BASE_URL;
break;
default:
this.baseUrl = networkOrUrl;
}
}

async getQuote(params: GatewayQuoteParams): Promise<GatewayQuote> {
const isMainnet = params.toChain == "bob" || params.toChain == 60808;

let outputToken = "";
if (params.toToken.startsWith("0x")) {
outputToken = params.toToken;
} else if (params.toToken in TOKENS) {
outputToken = isMainnet ? TOKENS[params.toToken].bob : TOKENS[params.toToken].bob_sepolia;
} else {
throw new Error('Unknown output token');
}

const atomicAmount = params.amount;
const response = await fetch(`${this.baseUrl}/quote/${outputToken}/${atomicAmount || ''}`, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
}
});

return await response.json();
}

// TODO: add error handling
async startOrder(gatewayQuote: GatewayQuote, params: GatewayQuoteParams): Promise<GatewayStartOrderResult> {
const request: GatewayCreateOrderRequest = {
gatewayAddress: gatewayQuote.gatewayAddress,
strategyAddress: gatewayQuote.strategyAddress,
satsToConvertToEth: params.gasRefill,
userAddress: params.toUserAddress,
// TODO: figure out how to get extra data
gatewayExtraData: null,
strategyExtraData: null,
satoshis: gatewayQuote.satoshis,
};

const response = await fetch(`${this.baseUrl}/order`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
body: JSON.stringify(request)
});

if (!response.ok) {
throw new Error('Failed to create order');
}

const data: GatewayCreateOrderResponse = await response.json();
// NOTE: could remove this check but good for sanity
if (data.opReturnHash != calculateOpReturnHash(request)) {
throw new Error('Invalid OP_RETURN hash');
}

return {
uuid: data.uuid,
opReturnHash: data.opReturnHash,
bitcoinAddress: gatewayQuote.bitcoinAddress,
satoshis: gatewayQuote.satoshis,
}
}

async finalizeOrder(orderUuid: string, bitcoinTx: string) {
const response = await fetch(`${this.baseUrl}/order/${orderUuid}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
body: JSON.stringify({ bitcoinTx: bitcoinTx })
});

if (!response.ok) {
throw new Error('Failed to update order');
}
}

async getOrders(userAddress: EvmAddress): Promise<GatewayOrderResponse[]> {
const response = await fetch(`${this.baseUrl}/orders/${userAddress}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
}
});

return response.json();
}

async getTokens(): Promise<EvmAddress[]> {
const response = await fetch(`${this.baseUrl}/tokens`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
}
});

return response.json();
}
}

function calculateOpReturnHash(req: GatewayCreateOrderRequest) {
const abiCoder = new AbiCoder();
return ethers.keccak256(abiCoder.encode(
["address", "address", "uint256", "address", "bytes", "bytes"],
[
req.gatewayAddress,
req.strategyAddress || ethers.ZeroAddress,
req.satsToConvertToEth,
req.userAddress,
req.gatewayExtraData,
req.strategyExtraData
]
))
}
3 changes: 3 additions & 0 deletions sdk/src/gateway/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

export { GatewayApiClient } from "./client";
export { GatewayQuoteParams } from "./types";
38 changes: 38 additions & 0 deletions sdk/src/gateway/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
type Token = {
name: string,
bob: string,
bob_sepolia: string
}

export const TOKENS: { [key: string]: Token } = {
"tBTC": {
name: "tBTC v2",
bob: "0xBBa2eF945D523C4e2608C9E1214C2Cc64D4fc2e2",
bob_sepolia: "0x6744bAbDf02DCF578EA173A9F0637771A9e1c4d0",
},
"WBTC": {
name: "Wrapped BTC",
bob: "0x03C7054BCB39f7b2e5B2c7AcB37583e32D70Cfa3",
bob_sepolia: "0xe51e40e15e6e1496a0981f90Ca1D632545bdB519",
},
"sbtBTC": {
name: "sb tBTC v2",
bob: "0x2925dF9Eb2092B53B06A06353A7249aF3a8B139e",
bob_sepolia: "",
},
"sbWBTC": {
name: "sb Wrapped BTC",
bob: "0x5c46D274ed8AbCAe2964B63c0360ad3Ccc384dAa",
bob_sepolia: "",
},
"seTBTC": {
name: "Segment TBTC",
bob: "0xD30288EA9873f376016A0250433b7eA375676077",
bob_sepolia: "",
},
"seWBTC": {
name: "Segment WBTC",
bob: "0x6265C05158f672016B771D6Fb7422823ed2CbcDd",
bob_sepolia: "",
}
}
32 changes: 32 additions & 0 deletions sdk/src/gateway/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
type ChainSlug = string | number;
type TokenSymbol = string;

export interface GatewayQuoteParams {
/** @description Source chain slug or ID */
fromChain: ChainSlug;
/** @description Destination chain slug or ID */
toChain: ChainSlug;
/** @description Token symbol or address on source chain */
fromToken: TokenSymbol;
/** @description Token symbol or address on destination chain */
toToken: TokenSymbol;
/** @description Wallet address on source chain */
fromUserAddress: string;
/** @description Wallet address on destination chain */
toUserAddress: string;
/** @description Amount of tokens to send from the source chain */
amount: number | string;

/** @description Maximum slippage percentage between 0.01 and 0.03 (Default: 0.03) */
maxSlippage?: number;

/** @description Unique affiliate ID for tracking */
affiliateId?: string;
/** @description Optionally filter the type of routes returned */
type?: 'swap' | 'deposit' | 'withdraw' | 'claim';
/** @description The percentage of fee charged by partners in Basis Points (BPS) units. This will override the default fee rate configured via platform. 1 BPS = 0.01%. The maximum value is 1000 (which equals 10%). The minimum value is 1 (which equals 0.01%). */
fee?: number;

/** @description Amount of satoshis to swap for ETH */
gasRefill?: number,
}

0 comments on commit fb8d901

Please sign in to comment.