Skip to content

Instantly share code, notes, and snippets.

@fadeev
Created June 18, 2024 10:53
Show Gist options
  • Save fadeev/5427683a991a5723fbace3ef6e775b85 to your computer and use it in GitHub Desktop.
Save fadeev/5427683a991a5723fbace3ef6e775b85 to your computer and use it in GitHub Desktop.
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}`
);
});
});
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}`;
};
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