-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Gregory Hill <[email protected]>
- Loading branch information
Showing
6 changed files
with
268 additions
and
89 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" || "bob": | ||
this.baseUrl = MAINNET_GATEWAY_BASE_URL; | ||
break; | ||
case "testnet" || "bob-sepolia": | ||
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 | ||
] | ||
)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { GatewayApiClient as GatewaySDK } from "./client"; | ||
export { GatewayQuoteParams } from "./types"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: "", | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |