diff --git a/README.md b/README.md index 0d9b7b0..b11e2ae 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ For more details about how to trigger it please see the `claimInternal` function It is also possible for someone else to pay for the claim fees. This can be useful if the funds deposited to pay for the claim transaction are not enough, or if someone wants to subsidize the claim. -The receiver can use the private key sign a message containing the address receiving the address (and optionally some address that will receive the dust). Using this signature, anybody can execute a transaction to perform the claim. To do so, they should call `claim_external` on the escrow account (through the `execute_action` entrypoint). +The receiver can use the private key sign a message containing the receiving address (and optionally some address that will receive the dust). Using this signature, anybody can execute a transaction to perform the claim. To do so, they should call `claim_external` on the escrow account (through the `execute_action` entrypoint). ![Sessions diagram](/docs/external_claim.png) @@ -87,9 +87,9 @@ The parameters are as follow: ## Local development We recommend you to install scarb through ASDF. Please refer to [these instructions](https://docs.swmansion.com/scarb/download.html#install-via-asdf). -Thanks to the [.tool-versions file](./.tool-versions), you don't need to install a specific scarb or starknet foundry version. The correct one will be automatically downloaded and installed. +Thanks to the [.tool-versions file](./.tool-versions), you can install the correct versions for scarb and starknet-foundry by running `asdf install`. -##@ Test the contracts (Cairo) +### Test the contracts (Cairo) ``` scarb test diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..5ccb7b5 Binary files /dev/null and b/bun.lockb differ diff --git a/lib/claim.ts b/lib/claim.ts index b16bf0b..6eb771e 100644 --- a/lib/claim.ts +++ b/lib/claim.ts @@ -3,6 +3,7 @@ import { Call, CallData, Calldata, + ProviderInterface, RPC, TransactionReceipt, UniversalDetails, @@ -146,19 +147,18 @@ export async function claimInternal(args: { gift: Gift; receiver: string; giftPrivateKey: string; + provider?: ProviderInterface; overrides?: { escrowAccountAddress?: string; callToAddress?: string }; details?: UniversalDetails; }): Promise { const escrowAddress = args.overrides?.escrowAccountAddress || calculateEscrowAddress(args.gift); - const escrowAccount = getEscrowAccount(args.gift, args.giftPrivateKey, escrowAddress); + const escrowAccount = getEscrowAccount(args.gift, args.giftPrivateKey, escrowAddress, args.provider); const response = await escrowAccount.execute( - [ - { - contractAddress: args.overrides?.callToAddress ?? escrowAddress, - calldata: [buildGiftCallData(args.gift), args.receiver], - entrypoint: "claim_internal", - }, - ], + { + contractAddress: args.overrides?.callToAddress ?? escrowAddress, + calldata: [buildGiftCallData(args.gift), args.receiver], + entrypoint: "claim_internal", + }, undefined, { ...args.details }, ); @@ -200,9 +200,14 @@ export const randomReceiver = (): string => { return `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; }; -export function getEscrowAccount(gift: Gift, giftPrivateKey: string, forceEscrowAddress?: string): Account { +export function getEscrowAccount( + gift: Gift, + giftPrivateKey: string, + forceEscrowAddress?: string, + provider?: ProviderInterface, +): Account { return new Account( - manager, + provider ?? manager, forceEscrowAddress || num.toHex(calculateEscrowAddress(gift)), giftPrivateKey, undefined, diff --git a/lib/deposit.ts b/lib/deposit.ts index 39c7f64..acb75ab 100644 --- a/lib/deposit.ts +++ b/lib/deposit.ts @@ -1,11 +1,62 @@ import { Account, Call, CallData, Contract, InvokeFunctionResponse, TransactionReceipt, hash, uint256 } from "starknet"; -import { AccountConstructorArguments, Gift, LegacyStarknetKeyPair, deployer, manager } from "./"; +import { AccountConstructorArguments, Gift, LegacyStarknetKeyPair, deployer, manager } from "."; export const STRK_GIFT_MAX_FEE = 200000000000000000n; // 0.2 STRK export const STRK_GIFT_AMOUNT = STRK_GIFT_MAX_FEE + 1n; export const ETH_GIFT_MAX_FEE = 200000000000000n; // 0.0002 ETH export const ETH_GIFT_AMOUNT = ETH_GIFT_MAX_FEE + 1n; +const depositAbi = [ + { + type: "function", + name: "deposit", + inputs: [ + { + name: "escrow_class_hash", + type: "core::starknet::class_hash::ClassHash", + }, + { + name: "gift_token", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "gift_amount", + type: "core::integer::u256", + }, + { + name: "fee_token", + type: "core::starknet::contract_address::ContractAddress", + }, + { + name: "fee_amount", + type: "core::integer::u128", + }, + { + name: "gift_pubkey", + type: "core::felt252", + }, + ], + outputs: [], + state_mutability: "external", + }, +]; + +const approveAbi = [ + { + type: "function", + name: "approve", + inputs: [ + { + name: "spender", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "amount", type: "core::integer::u256" }, + ], + outputs: [{ type: "core::bool" }], + state_mutability: "external", + }, +]; + export function getMaxFee(useTxV3: boolean): bigint { return useTxV3 ? STRK_GIFT_MAX_FEE : ETH_GIFT_MAX_FEE; } @@ -14,42 +65,37 @@ export function getGiftAmount(useTxV3: boolean): bigint { return useTxV3 ? STRK_GIFT_AMOUNT : ETH_GIFT_AMOUNT; } -export async function deposit(depositParams: { - sender: Account; +interface DepositParams { giftAmount: bigint; feeAmount: bigint; factoryAddress: string; feeTokenAddress: string; giftTokenAddress: string; giftSignerPubKey: bigint; - overrides?: { - escrowAccountClassHash?: string; - }; -}): Promise<{ response: InvokeFunctionResponse; gift: Gift }> { - const { sender, giftAmount, feeAmount, factoryAddress, feeTokenAddress, giftTokenAddress, giftSignerPubKey } = - depositParams; - const factory = await manager.loadContract(factoryAddress); - const feeToken = await manager.loadContract(feeTokenAddress); - const giftToken = await manager.loadContract(giftTokenAddress); + escrowAccountClassHash: string; +} - const escrowAccountClassHash = - depositParams.overrides?.escrowAccountClassHash || (await factory.get_latest_escrow_class_hash()); - const gift: Gift = { - factory: factoryAddress, - escrow_class_hash: escrowAccountClassHash, - sender: deployer.address, - gift_token: giftTokenAddress, - gift_amount: giftAmount, - fee_token: feeTokenAddress, - fee_amount: feeAmount, - gift_pubkey: giftSignerPubKey, - }; - const calls: Array = []; +export function createDeposit( + sender: string, + { + giftAmount, + feeAmount, + factoryAddress, + feeTokenAddress, + giftTokenAddress, + giftSignerPubKey, + escrowAccountClassHash, + }: DepositParams, +) { + const factory = new Contract(depositAbi, factoryAddress); + const feeToken = new Contract(approveAbi, feeTokenAddress); + const giftToken = new Contract(approveAbi, giftTokenAddress); + const calls: Call[] = []; if (feeTokenAddress === giftTokenAddress) { - calls.push(feeToken.populateTransaction.approve(factory.address, giftAmount + feeAmount)); + calls.push(feeToken.populateTransaction.approve(factoryAddress, giftAmount + feeAmount)); } else { - calls.push(feeToken.populateTransaction.approve(factory.address, feeAmount)); - calls.push(giftToken.populateTransaction.approve(factory.address, giftAmount)); + calls.push(feeToken.populateTransaction.approve(factoryAddress, feeAmount)); + calls.push(giftToken.populateTransaction.approve(factoryAddress, giftAmount)); } calls.push( factory.populateTransaction.deposit( @@ -61,10 +107,26 @@ export async function deposit(depositParams: { giftSignerPubKey, ), ); - return { - response: await sender.execute(calls), - gift, + const gift: Gift = { + factory: factoryAddress, + escrow_class_hash: escrowAccountClassHash, + sender, + gift_token: giftTokenAddress, + gift_amount: giftAmount, + fee_token: feeTokenAddress, + fee_amount: feeAmount, + gift_pubkey: giftSignerPubKey, }; + return { calls, gift }; +} + +export async function deposit( + sender: Account, + depositParams: DepositParams, +): Promise<{ response: InvokeFunctionResponse; gift: Gift }> { + const { calls, gift } = createDeposit(sender.address, depositParams); + const response = await sender.execute(calls); + return { response, gift }; } export async function defaultDepositTestSetup(args: { @@ -97,15 +159,14 @@ export async function defaultDepositTestSetup(args: { const giftSigner = new LegacyStarknetKeyPair(args.overrides?.giftPrivateKey); const giftPubKey = giftSigner.publicKey; - const { response, gift } = await deposit({ - sender: deployer, - overrides: { escrowAccountClassHash }, + const { response, gift } = await deposit(deployer, { giftAmount, feeAmount, factoryAddress: args.factory.address, feeTokenAddress: feeToken.address, giftTokenAddress, giftSignerPubKey: giftPubKey, + escrowAccountClassHash, }); const txReceipt = await manager.waitForTransaction(response.transaction_hash); return { gift, giftPrivateKey: giftSigner.privateKey, txReceipt }; @@ -121,14 +182,7 @@ export function calculateEscrowAddress(gift: Gift): string { gift_pubkey: gift.gift_pubkey, }; - const escrowAddress = hash.calculateContractAddressFromHash( - 0, - gift.escrow_class_hash, - CallData.compile({ - ...constructorArgs, - gift_amount: uint256.bnToUint256(gift.gift_amount), - }), - gift.factory, - ); + const calldata = CallData.compile({ ...constructorArgs, gift_amount: uint256.bnToUint256(gift.gift_amount) }); + const escrowAddress = hash.calculateContractAddressFromHash(0, gift.escrow_class_hash, calldata, gift.factory); return escrowAddress; } diff --git a/scripts/claim_gift.ts b/scripts/claim_gift.ts new file mode 100644 index 0000000..ea2fd49 --- /dev/null +++ b/scripts/claim_gift.ts @@ -0,0 +1,22 @@ +import { constants, RpcProvider } from "starknet"; +import { claimInternal } from "../lib"; + +/// To use this script, fill in the following 3 variables printed from scripts/create_gift.ts: + +const gift = { + factory: "0x000000000000000000000000000000000000000000000000000000000000000", + escrow_class_hash: "0x000000000000000000000000000000000000000000000000000000000000000", + sender: "0x0000000000000000000000000000000000000000000000000000000000000000", + gift_token: "0x0000000000000000000000000000000000000000000000000000000000000000", + gift_amount: 69n, + fee_token: "0x0000000000000000000000000000000000000000000000000000000000000000", + fee_amount: 42n, + gift_pubkey: 100000000000000000000000000000000000000000000000000000000000000000000000000n, +} ; +const receiver = "0x0000000000000000000000000000000000000000000000000000000000000000"; +const giftPrivateKey = "0x0000000000000000000000000000000000000000000000000000000000000000"; + +const provider = new RpcProvider({ nodeUrl: constants.NetworkName.SN_SEPOLIA }); +const receipt = await claimInternal({ gift, receiver, giftPrivateKey, provider }); + +console.log("Tx hash:", receipt.transaction_hash); diff --git a/scripts/create_gift.ts b/scripts/create_gift.ts new file mode 100644 index 0000000..9d2f7a1 --- /dev/null +++ b/scripts/create_gift.ts @@ -0,0 +1,36 @@ +import { createDeposit, LegacyStarknetKeyPair } from "../lib"; +import { logTransactionJson } from "./json_tx_builder"; + +/// To use this script, check the following value: + +const factoryAddress = "0x42a18d85a621332f749947a96342ba682f08e499b9f1364325903a37c5def60"; +const escrowAccountClassHash = "0x661aad3c9812f0dc0a78f320a58bdd8fed18ef601245c20e4bf43667bfd0289"; +const ethAddress = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"; +const strkAddress = "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"; + +if (!factoryAddress) { + throw new Error("Factory contract address is not set. Please set it in the script file."); +} + +const giftSigner = new LegacyStarknetKeyPair(); +const sender = "0x1111111111111111111111111111111111111111111111111111111111111111"; +const receiver = "0x2222222222222222222222222222222222222222222222222222222222222222"; + +const { calls, gift } = createDeposit(sender, { + giftAmount: 1n, // 1 wei + feeAmount: 3n * 10n ** 18n, // 3 STRK + factoryAddress, + feeTokenAddress: strkAddress, + giftTokenAddress: ethAddress, + giftSignerPubKey: giftSigner.publicKey, + escrowAccountClassHash, +}); + +console.log(); +console.log("const gift =", gift, ";"); +console.log(`const receiver = "${receiver}";`); +console.log(`const giftPrivateKey = "${giftSigner.privateKey}";`); +console.log(); + +console.log("Calls:"); +logTransactionJson(calls); diff --git a/tests-integration/factory.test.ts b/tests-integration/factory.test.ts index 9b8c66e..4929f93 100644 --- a/tests-integration/factory.test.ts +++ b/tests-integration/factory.test.ts @@ -75,7 +75,7 @@ describe("Test Core Factory Functions", function () { it(`Pausable`, async function () { // Deploy factory - const { factory } = await setupGiftProtocol(); + const { factory, escrowAccountClassHash } = await setupGiftProtocol(); const receiver = randomReceiver(); const giftSigner = new LegacyStarknetKeyPair(); @@ -87,14 +87,14 @@ describe("Test Core Factory Functions", function () { await manager.waitForTransaction(txHash1); await expectRevertWithErrorMessage("Pausable: paused", async () => { - const { response } = await deposit({ - sender: deployer, + const { response } = await deposit(deployer, { giftAmount: ETH_GIFT_AMOUNT, feeAmount: ETH_GIFT_MAX_FEE, factoryAddress: factory.address, feeTokenAddress: token.address, giftTokenAddress: token.address, giftSignerPubKey: giftSigner.publicKey, + escrowAccountClassHash, }); return response; });