-
-
Save fadeev/5427683a991a5723fbace3ef6e775b85 to your computer and use it in GitHub Desktop.
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
import { ethers } from "ethers"; | |
import { describe, expect, it } from "vitest"; | |
import { getBitcoinMemoData } from "./bitcoin-memo-data"; | |
const firstExampleAddress = "0x0987654321098765432109876543210987654321"; | |
const firstExampleAddressWithout0x = firstExampleAddress.replace(/0x/i, ""); | |
const secondExampleAddress = "0x1234567890123456789012345678901234567890"; | |
const secondExampleAddressWithout0x = secondExampleAddress.replace(/0x/i, ""); | |
const chainId = "123"; | |
const hexChainId = ethers.utils.hexlify(+chainId); | |
const paddedChainId = hexChainId.replace(/0x/i, "").padStart(8, "0"); | |
describe("Bitcoin Memo data", () => { | |
it("Returns only the receiver address without leading 0x if nothing else is provided", () => { | |
expect(getBitcoinMemoData({ to: firstExampleAddress })).toBe(firstExampleAddressWithout0x); | |
}); | |
it("Throws an error if the 'to' address is empty", () => { | |
expect(() => getBitcoinMemoData({ to: "" })).toThrow(); | |
}); | |
it("Returns the address and padded chain ID if chain ID is provided", () => { | |
expect(getBitcoinMemoData({ to: firstExampleAddress, chainId })).toBe( | |
`${firstExampleAddressWithout0x}${paddedChainId}` | |
); | |
}); | |
it("Returns the contract address and recipient address if contract address is provided", () => { | |
expect(getBitcoinMemoData({ to: firstExampleAddress, contractAddress: secondExampleAddress })).toBe( | |
`${secondExampleAddressWithout0x}${firstExampleAddressWithout0x}` | |
); | |
}); | |
it("Returns contract address, recipient address, and padded chain ID if all are provided", () => { | |
expect(getBitcoinMemoData({ to: firstExampleAddress, contractAddress: secondExampleAddress, chainId })).toBe( | |
`${secondExampleAddressWithout0x}${firstExampleAddressWithout0x}${paddedChainId}` | |
); | |
}); | |
it("Correctly removes leading '0x' from all input fields", () => { | |
expect(getBitcoinMemoData({ to: firstExampleAddress, contractAddress: secondExampleAddress, chainId })).toBe( | |
`${secondExampleAddressWithout0x}${firstExampleAddressWithout0x}${paddedChainId}` | |
); | |
}); | |
}); |
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
import { ethers } from "ethers"; | |
import { z } from "zod"; | |
const removeLeading0x = (str: string) => str.replace(/^0x/, ""); | |
const toAddressSchema = z | |
.string() | |
.refine((address) => ethers.utils.isAddress(address), { | |
message: "Invalid recipient address", | |
}) | |
.transform(removeLeading0x); | |
const contractAddressSchema = z | |
.string() | |
.refine((address) => ethers.utils.isAddress(address), { | |
message: "Invalid contract address", | |
}) | |
.transform(removeLeading0x) | |
.optional(); | |
const chainIdSchema = z | |
.string() | |
.transform((chainId) => { | |
const hexChainId = ethers.utils.hexlify(+chainId); | |
const paddedChainId = removeLeading0x(hexChainId).padStart(8, "0"); | |
return paddedChainId; | |
}) | |
.optional(); | |
const getMemoDataSchema = z.object({ | |
to: toAddressSchema, | |
contractAddress: contractAddressSchema, | |
chainId: chainIdSchema, | |
}); | |
type GetMemoDataParams = z.infer<typeof getMemoDataSchema>; | |
/** | |
* The memo `[DATA]` is an array of bytes that encodes the recipient address of this deposit into ZRC-20 | |
* or the smart contract on zEVM that will be invoked by this transaction. | |
* | |
* 1. If the purpose is to only deposit BTC into the BTC ZRC-20 on zEVM, | |
* then the `[DATA]` should be exactly 20 bytes long, consists of an Ethereum-style address. | |
* | |
* 2. If the purpose is to deposit BTC and also use the deposited amount to call a smart contract on zEVM, | |
* then the `[DATA]` field must consists of a smart contract address, and a binary message that will be forwarded | |
* to the said smart contract: `[DATA] = [zEVM contract address (20B)] + [arbitrary binary message]` | |
* | |
* @docs https://www.zetachain.com/docs/developers/omnichain/bitcoin | |
*/ | |
export const getBitcoinMemoData = (params: GetMemoDataParams) => { | |
const { to, contractAddress = "", chainId = "" } = getMemoDataSchema.parse(params); | |
return `${contractAddress}${to}${chainId}`; | |
}; |
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
import { isServer } from "../../constants/app.constants"; | |
import { getBitcoinMemoData } from "./bitcoin-memo-data"; | |
interface XDEFIBitcoinTransactionParams { | |
from: string; | |
recipient: string; | |
amount: { | |
amount: string; | |
decimals: string; | |
}; | |
memo: string; | |
feeRate?: number; | |
} | |
/** | |
* @docs https://www.zetachain.com/docs/developers/omnichain/tutorials/bitcoin-frontend/#use-xdefi-wallet | |
*/ | |
const sendXDEFIBitcoinTransaction = ({ | |
from, | |
to, | |
tssAddress, | |
contractAddress, | |
amount, | |
chainId, | |
}: SendBitcoinTransactionParams) => { | |
const { xfi } = window as any; | |
return new Promise<string>((resolve, reject) => { | |
const xdefiBtcTxParams: XDEFIBitcoinTransactionParams = { | |
from, | |
recipient: tssAddress, | |
memo: `hex::${getBitcoinMemoData({ to, contractAddress, chainId })}`, | |
amount: { | |
amount, | |
decimals: "8", | |
}, | |
}; | |
xfi.bitcoin.request( | |
{ | |
method: "transfer", | |
params: [xdefiBtcTxParams], | |
}, | |
(error: any, txHash: string) => { | |
if (error) { | |
reject(error || "Error sending XDEFI Bitcoin transaction"); | |
} else { | |
resolve(txHash); | |
} | |
} | |
); | |
}); | |
}; | |
interface OKXBitcoinTransactionParams { | |
from: string; | |
to: string; | |
value: number; | |
memo: string; | |
/** | |
* Memo should be in the second position of the outputs | |
* @docs https://www.zetachain.com/docs/developers/omnichain/bitcoin/#:~:text=The%20second%20output%20must%20be%20a%20memo%20output%2C%20i.e.%20OP_RETURN%20PUSH_x%20%5BDATA%5D.%20This%20output%20must%20be%20less%20than%2080%20bytes. | |
*/ | |
memoPos: 1; | |
} | |
/** | |
* @docs https://www.okx.com/web3/build/docs/sdks/chains/bitcoin/provider | |
*/ | |
const sendOKXBitcoinTransaction = async ({ | |
from, | |
to, | |
tssAddress, | |
contractAddress, | |
amount, | |
chainId, | |
}: SendBitcoinTransactionParams) => { | |
const { okxwallet } = window as any; | |
const okxBtcTxParams: OKXBitcoinTransactionParams = { | |
from, | |
to: tssAddress, | |
value: +amount / 1e8, | |
memo: `0x${getBitcoinMemoData({ to, contractAddress, chainId })}`, | |
memoPos: 1, | |
}; | |
try { | |
const { txhash } = await okxwallet.bitcoin.send(okxBtcTxParams); | |
return txhash; | |
} catch (error: any) { | |
throw new Error(error.message || "Error sending OKX Bitcoin transaction"); | |
} | |
}; | |
interface SendBitcoinTransactionParams { | |
from: string; | |
to: string; | |
tssAddress: string; | |
contractAddress?: string; | |
amount: string; | |
chainId?: string; | |
} | |
export const sendBitcoinTransaction = async (txParams: SendBitcoinTransactionParams) => { | |
if (isServer) { | |
throw new Error("Method not available on server"); | |
} | |
const isOkxInWindow = "okxwallet" in window; | |
const isXdefiInWindow = "xfi" in window; | |
const { xfi, okxwallet } = window as any; | |
if (isOkxInWindow && okxwallet.bitcoin) { | |
return sendOKXBitcoinTransaction(txParams); | |
} | |
if (isXdefiInWindow && xfi.bitcoin) { | |
return sendXDEFIBitcoinTransaction(txParams); | |
} | |
throw new Error("No Bitcoin wallet found"); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment