diff --git a/Dockerfile b/Dockerfile index 3487c75..d9a9554 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # Use the base image -FROM shardlabs/starknet-devnet-rs:c4185522228f61ba04619151eb5706d4610fb00f +FROM shardlabs/starknet-devnet-rs:bab781a018318df51adb20fc60716c8429ee89b0 # Expose port 5050 EXPOSE 5050 # Set default command to run the container -CMD ["--gas-price", "36000000000", "--data-gas-price", "1", "--timeout", "320", "--seed", "0"] \ No newline at end of file +CMD ["--gas-price", "36000000000", "--data-gas-price", "1", "--timeout", "320", "--seed", "0", "--lite-mode", "--gas-price-strk", "36000000000", "--data-gas-price-strk", "1"] \ No newline at end of file diff --git a/lib/accounts.ts b/lib/accounts.ts index e9858d6..417183a 100644 --- a/lib/accounts.ts +++ b/lib/accounts.ts @@ -1,99 +1,7 @@ -import { - Abi, - Account, - AllowArray, - ArraySignatureType, - CairoOption, - CairoOptionVariant, - Call, - CallData, - Contract, - DeployAccountContractPayload, - DeployContractResponse, - GetTransactionReceiptResponse, - InvocationsSignerDetails, - InvokeFunctionResponse, - RPC, - RawCalldata, - Signature, - UniversalDetails, - V2InvocationsSignerDetails, - V3InvocationsSignerDetails, - hash, - num, - shortString, - stark, - transaction, - uint256, -} from "starknet"; +import { Account, Call, CallData, RPC, uint256 } from "starknet"; import { manager } from "./manager"; -import { ensureSuccess } from "./receipts"; -import { LegacyArgentSigner, LegacyKeyPair, LegacyMultisigSigner, LegacyStarknetKeyPair } from "./signers/legacy"; -import { ArgentSigner, KeyPair, RawSigner, randomStarknetKeyPair } from "./signers/signers"; import { ethAddress, strkAddress } from "./tokens"; -export const VALID = BigInt(shortString.encodeShortString("VALID")); - -export class ArgentAccount extends Account { - // Increase the gas limit by 30% to avoid failures due to gas estimation being too low with tx v3 and transactions the use escaping - override async deployAccount( - payload: DeployAccountContractPayload, - details?: UniversalDetails, - ): Promise { - details ||= {}; - if (!details.skipValidate) { - details.skipValidate = false; - } - return super.deployAccount(payload, details); - } - - override async execute( - calls: AllowArray, - abis?: Abi[], - details: UniversalDetails = {}, - ): Promise { - details ||= {}; - if (!details.skipValidate) { - details.skipValidate = false; - } - if (details.resourceBounds) { - return super.execute(calls, abis, details); - } - const estimate = await this.estimateFee(calls, details); - return super.execute(calls, abis, { - ...details, - resourceBounds: { - ...estimate.resourceBounds, - l1_gas: { - ...estimate.resourceBounds.l1_gas, - max_amount: num.toHexString(num.addPercent(estimate.resourceBounds.l1_gas.max_amount, 30)), - }, - }, - }); - } -} - -export interface ArgentWallet { - account: ArgentAccount; - accountContract: Contract; - owner: KeyPair; -} - -export interface ArgentWalletWithGuardian extends ArgentWallet { - guardian: KeyPair; -} - -export interface LegacyArgentWallet { - account: ArgentAccount; - accountContract: Contract; - owner: LegacyKeyPair; - guardian: LegacyKeyPair; -} - -export interface ArgentWalletWithGuardianAndBackup extends ArgentWalletWithGuardian { - guardianBackup: KeyPair; -} - export const deployer = (() => { if (manager.isDevnet) { const devnetAddress = "0x64b48806902a367c8598f4f95c305e8c1a1acba5f082d294a43793113115691"; @@ -119,265 +27,20 @@ export const genericAccount = (() => { export const deployerV3 = setDefaultTransactionVersionV3(deployer); -export function setDefaultTransactionVersion(account: ArgentAccount, newVersion: boolean): Account { +export function setDefaultTransactionVersion(account: Account, newVersion: boolean): Account { const newDefaultVersion = newVersion ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; if (account.transactionVersion === newDefaultVersion) { return account; } - return new ArgentAccount(account, account.address, account.signer, account.cairoVersion, newDefaultVersion); + return new Account(account, account.address, account.signer, account.cairoVersion, newDefaultVersion); } -export function setDefaultTransactionVersionV3(account: ArgentAccount): ArgentAccount { +export function setDefaultTransactionVersionV3(account: Account): Account { return setDefaultTransactionVersion(account, true); } console.log("Deployer:", deployer.address); -export async function deployOldAccount( - owner = new LegacyStarknetKeyPair(), - guardian = new LegacyStarknetKeyPair(), - salt = num.toHex(randomStarknetKeyPair().privateKey), -): Promise { - const proxyClassHash = await manager.declareFixtureContract("Proxy"); - const oldArgentAccountClassHash = await manager.declareFixtureContract("OldArgentAccount"); - - const constructorCalldata = CallData.compile({ - implementation: oldArgentAccountClassHash, - selector: hash.getSelectorFromName("initialize"), - calldata: CallData.compile({ owner: owner.publicKey, guardian: guardian.publicKey }), - }); - - const contractAddress = hash.calculateContractAddressFromHash(salt, proxyClassHash, constructorCalldata, 0); - - const account = new Account(manager, contractAddress, owner); - account.signer = new LegacyMultisigSigner([owner, guardian]); - - await fundAccount(account.address, 1e16, "ETH"); // 0.01 ETH - - const { transaction_hash } = await account.deployAccount({ - classHash: proxyClassHash, - constructorCalldata, - contractAddress, - addressSalt: salt, - }); - await manager.waitForTransaction(transaction_hash); - const accountContract = await manager.loadContract(account.address); - accountContract.connect(account); - return { account, accountContract, owner, guardian }; -} - -async function deployAccountInner(params: DeployAccountParams): Promise< - DeployAccountParams & { - account: Account; - classHash: string; - owner: KeyPair; - guardian?: KeyPair; - salt: string; - transactionHash: string; - } -> { - const finalParams = { - ...params, - classHash: params.classHash ?? (await manager.declareLocalContract("ArgentAccount")), - salt: params.salt ?? num.toHex(randomStarknetKeyPair().privateKey), - owner: params.owner ?? randomStarknetKeyPair(), - useTxV3: params.useTxV3 ?? false, - selfDeploy: params.selfDeploy ?? false, - }; - const guardian = finalParams.guardian - ? finalParams.guardian.signerAsOption - : new CairoOption(CairoOptionVariant.None); - const constructorCalldata = CallData.compile({ owner: finalParams.owner.signer, guardian }); - - const { classHash, salt } = finalParams; - const contractAddress = hash.calculateContractAddressFromHash(salt, classHash, constructorCalldata, 0); - const fundingCall = finalParams.useTxV3 - ? await fundAccountCall(contractAddress, finalParams.fundingAmount ?? 1e16, "STRK") // 0.01 STRK - : await fundAccountCall(contractAddress, finalParams.fundingAmount ?? 1e18, "ETH"); // 1 ETH - const calls = fundingCall ? [fundingCall] : []; - - const transactionVersion = finalParams.useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; - const signer = new ArgentSigner(finalParams.owner, finalParams.guardian); - const account = new ArgentAccount(manager, contractAddress, signer, "1", transactionVersion); - - let transactionHash; - if (finalParams.selfDeploy) { - const response = await deployer.execute(calls); - await manager.waitForTransaction(response.transaction_hash); - const { transaction_hash } = await account.deploySelf({ classHash, constructorCalldata, addressSalt: salt }); - transactionHash = transaction_hash; - } else { - const udcCalls = deployer.buildUDCContractPayload({ classHash, salt, constructorCalldata, unique: false }); - const { transaction_hash } = await deployer.execute([...calls, ...udcCalls]); - transactionHash = transaction_hash; - } - - await manager.waitForTransaction(transactionHash); - return { ...finalParams, account, transactionHash }; -} - -export type DeployAccountParams = { - useTxV3?: boolean; - classHash?: string; - owner?: KeyPair; - guardian?: KeyPair; - salt?: string; - fundingAmount?: number | bigint; - selfDeploy?: boolean; -}; - -export async function deployAccount( - params: DeployAccountParams = {}, -): Promise { - params.guardian ||= randomStarknetKeyPair(); - const { account, owner, transactionHash } = await deployAccountInner(params); - const accountContract = await manager.loadContract(account.address); - accountContract.connect(account); - return { account, accountContract, owner, guardian: params.guardian, transactionHash }; -} - -export async function deployAccountWithoutGuardian( - params: Omit = {}, -): Promise { - const { account, owner, transactionHash } = await deployAccountInner(params); - const accountContract = await manager.loadContract(account.address); - accountContract.connect(account); - return { account, accountContract, owner, transactionHash }; -} - -export async function deployAccountWithGuardianBackup( - params: DeployAccountParams & { guardianBackup?: KeyPair } = {}, -): Promise { - const guardianBackup = params.guardianBackup ?? randomStarknetKeyPair(); - - const wallet = (await deployAccount(params)) as ArgentWalletWithGuardianAndBackup & { transactionHash: string }; - await wallet.accountContract.change_guardian_backup(guardianBackup.compiledSignerAsOption); - - wallet.account.signer = new ArgentSigner(wallet.owner, guardianBackup); - wallet.guardianBackup = guardianBackup; - wallet.accountContract.connect(wallet.account); - return wallet; -} - -export async function deployLegacyAccount(classHash: string) { - const owner = new LegacyStarknetKeyPair(); - const guardian = new LegacyStarknetKeyPair(); - const salt = num.toHex(owner.privateKey); - const constructorCalldata = CallData.compile({ owner: owner.publicKey, guardian: guardian.publicKey }); - const contractAddress = hash.calculateContractAddressFromHash(salt, classHash, constructorCalldata, 0); - await fundAccount(contractAddress, 1e15, "ETH"); // 0.001 ETH - const account = new Account(manager, contractAddress, owner, "1"); - account.signer = new LegacyArgentSigner(owner, guardian); - - const { transaction_hash } = await account.deploySelf({ - classHash, - constructorCalldata, - addressSalt: salt, - }); - await manager.waitForTransaction(transaction_hash); - - const accountContract = await manager.loadContract(account.address); - accountContract.connect(account); - return { account, accountContract, owner, guardian }; -} - -export async function upgradeAccount( - accountToUpgrade: Account, - newClassHash: string, - calldata: RawCalldata = [], -): Promise { - const { transaction_hash } = await accountToUpgrade.execute( - { - contractAddress: accountToUpgrade.address, - entrypoint: "upgrade", - calldata: CallData.compile({ implementation: newClassHash, calldata }), - }, - undefined, - { maxFee: 1e14 }, - ); - return await ensureSuccess(await manager.waitForTransaction(transaction_hash)); -} - -export async function executeWithCustomSig( - account: ArgentAccount, - transactions: AllowArray, - signature: ArraySignatureType, - transactionsDetail: UniversalDetails = {}, -): Promise { - const signer = new (class extends RawSigner { - public async signRaw(messageHash: string): Promise { - return signature; - } - })(); - const newAccount = new ArgentAccount( - manager, - account.address, - signer, - account.cairoVersion, - account.transactionVersion, - ); - - return await newAccount.execute(transactions, undefined, transactionsDetail); -} - -export async function getSignerDetails(account: ArgentAccount, calls: Call[]): Promise { - const newAccount = new ArgentAccount( - manager, - account.address, - account.signer, - account.cairoVersion, - account.transactionVersion, - ); - const customSigner = new (class extends RawSigner { - public signerDetails?: InvocationsSignerDetails; - public async signTransaction(calls: Call[], signerDetails: InvocationsSignerDetails): Promise { - this.signerDetails = signerDetails; - throw Error("Should not execute"); - } - public async signRaw(messageHash: string): Promise { - throw Error("Not implemented"); - } - })(); - newAccount.signer = customSigner; - try { - await newAccount.execute(calls, undefined); - throw Error("Should not execute"); - } catch (customError) { - return customSigner.signerDetails!; - } -} - -export function calculateTransactionHash(transactionDetail: InvocationsSignerDetails, calls: Call[]): string { - const compiledCalldata = transaction.getExecuteCalldata(calls, transactionDetail.cairoVersion); - let transactionHash; - if (Object.values(RPC.ETransactionVersion2).includes(transactionDetail.version as any)) { - const transactionDetailV2 = transactionDetail as V2InvocationsSignerDetails; - transactionHash = hash.calculateInvokeTransactionHash({ - ...transactionDetailV2, - senderAddress: transactionDetailV2.walletAddress, - compiledCalldata, - }); - } else if (Object.values(RPC.ETransactionVersion3).includes(transactionDetail.version as any)) { - const transactionDetailV3 = transactionDetail as V3InvocationsSignerDetails; - transactionHash = hash.calculateInvokeTransactionHash({ - ...transactionDetailV3, - senderAddress: transactionDetailV3.walletAddress, - compiledCalldata, - nonceDataAvailabilityMode: stark.intDAM(transactionDetailV3.nonceDataAvailabilityMode), - feeDataAvailabilityMode: stark.intDAM(transactionDetailV3.feeDataAvailabilityMode), - }); - } else { - throw Error("unsupported transaction version"); - } - return transactionHash; -} - -export async function fundAccount(recipient: string, amount: number | bigint, token: "ETH" | "STRK") { - const call = await fundAccountCall(recipient, amount, token); - const response = await deployer.execute(call ? [call] : []); - await manager.waitForTransaction(response.transaction_hash); -} - export async function fundAccountCall( recipient: string, amount: number | bigint, diff --git a/lib/claim.ts b/lib/claim.ts index 1f26b67..dd95d09 100644 --- a/lib/claim.ts +++ b/lib/claim.ts @@ -1,5 +1,5 @@ -import { shortString } from "starknet"; -import { manager } from "."; +import { Account, RPC, TransactionReceipt, UniversalDetails, ec, encode, num, shortString, uint256 } from "starknet"; +import { LegacyStarknetKeyPair, calculateClaimAddress, deployer, ethAddress, manager, strkAddress } from "."; const typesRev1 = { StarknetDomain: [ @@ -35,3 +35,80 @@ export async function getClaimExternalData(claimExternal: ClaimExternal) { message: { ...claimExternal }, }; } + +export interface AccountConstructorArguments { + sender: string; + gift_token: string; + gift_amount: bigint; + fee_token: string; + fee_amount: bigint; + claim_pubkey: bigint; +} + +export interface Claim extends AccountConstructorArguments { + factory: string; + class_hash: string; +} + +export function buildCallDataClaim(claim: Claim) { + return { + ...claim, + gift_amount: uint256.bnToUint256(claim.gift_amount), + }; +} + +export async function claimExternal( + claim: Claim, + receiver: string, + giftPrivateKey: string, + account = deployer, +): Promise { + const claimAddress = calculateClaimAddress(claim); + const giftSigner = new LegacyStarknetKeyPair(giftPrivateKey); + const claimExternalData = await getClaimExternalData({ receiver }); + const signature = await giftSigner.signMessage(claimExternalData, claimAddress); + + return (await account.execute([ + { + contractAddress: claim.factory, + calldata: [buildCallDataClaim(claim), receiver, signature], + entrypoint: "claim_external", + }, + ])) as TransactionReceipt; +} + +export async function claimInternal( + claim: Claim, + receiver: string, + claimSignerPrivateKey: string, + details?: UniversalDetails, +): Promise { + const claimAddress = calculateClaimAddress(claim); + + const txVersion = useTxv3(claim.fee_token) ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; + const claimAccount = new Account(manager, num.toHex(claimAddress), claimSignerPrivateKey, undefined, txVersion); + return (await claimAccount.execute( + [ + { + contractAddress: claim.factory, + calldata: [buildCallDataClaim(claim), receiver], + entrypoint: "claim_internal", + }, + ], + undefined, + { ...details }, + )) as TransactionReceipt; +} + +function useTxv3(tokenAddress: string): boolean { + if (tokenAddress === ethAddress) { + return false; + } else if (tokenAddress === strkAddress) { + return true; + } + throw new Error(`Unsupported token`); +} + +export const randomReceiver = (): string => { + return `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; +}; diff --git a/lib/deposit.ts b/lib/deposit.ts new file mode 100644 index 0000000..c2b66f9 --- /dev/null +++ b/lib/deposit.ts @@ -0,0 +1,97 @@ +import { Account, CallData, Contract, ec, encode, hash, uint256 } from "starknet"; +import { AccountConstructorArguments, Claim, LegacyStarknetKeyPair, deployer, manager } from "./"; + +export const GIFT_AMOUNT = 1000000000000000n; +export const GIFT_MAX_FEE = 50000000000000n; + +export async function deposit( + sender: Account, + giftAmount: bigint, + feeAmount: bigint, + factoryAddress: string, + feeTokenAddress: string, + giftTokenAddress: string, + claimSignerPubKey: bigint, +) { + const factory = await manager.loadContract(factoryAddress); + const feeToken = await manager.loadContract(feeTokenAddress); + const giftToken = await manager.loadContract(giftTokenAddress); + if (feeTokenAddress === giftTokenAddress) { + await sender.execute([ + feeToken.populateTransaction.approve(factory.address, giftAmount + feeAmount), + factory.populateTransaction.deposit(giftTokenAddress, giftAmount, feeTokenAddress, feeAmount, claimSignerPubKey), + ]); + } else { + await sender.execute([ + feeToken.populateTransaction.approve(factory.address, feeAmount), + giftToken.populateTransaction.approve(factory.address, giftAmount), + factory.populateTransaction.deposit(giftTokenAddress, giftAmount, feeTokenAddress, feeAmount, claimSignerPubKey), + ]); + } +} + +export async function defaultDepositTestSetup( + factory: Contract, + useTxV3 = false, + giftPrivateKey?: string, + giftTokenAddress?: string, + giftAmount = GIFT_AMOUNT, + giftMaxFee = GIFT_MAX_FEE, +): Promise<{ + claim: Claim; + claimPrivateKey: string; +}> { + const tokenContract = await manager.tokens.feeTokenContract(useTxV3); + + // static signer for gas profiling + const claimSigner = new LegacyStarknetKeyPair( + giftPrivateKey || `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`, + ); + const claimPubKey = claimSigner.publicKey; + await deposit( + deployer, + giftAmount, + giftMaxFee, + factory.address, + tokenContract.address, + giftTokenAddress || tokenContract.address, + claimPubKey, + ); + + const claimClassHash = await factory.get_latest_claim_class_hash(); + + const claim: Claim = { + factory: factory.address, + class_hash: claimClassHash, + sender: deployer.address, + gift_token: giftTokenAddress || tokenContract.address, + gift_amount: giftAmount, + fee_token: tokenContract.address, + fee_amount: giftMaxFee, + claim_pubkey: claimPubKey, + }; + + return { claim, claimPrivateKey: claimSigner.privateKey }; +} + +export function calculateClaimAddress(claim: Claim): string { + const constructorArgs: AccountConstructorArguments = { + sender: claim.sender, + gift_token: claim.gift_token, + gift_amount: claim.gift_amount, + fee_token: claim.fee_token, + fee_amount: claim.fee_amount, + claim_pubkey: claim.claim_pubkey, + }; + + const claimAddress = hash.calculateContractAddressFromHash( + 0, + claim.class_hash, + CallData.compile({ + ...constructorArgs, + gift_amount: uint256.bnToUint256(claim.gift_amount), + }), + claim.factory, + ); + return claimAddress; +} diff --git a/lib/index.ts b/lib/index.ts index 3116f82..04c710a 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -7,20 +7,14 @@ chai.should(); export * from "./accounts"; export * from "./claim"; export * from "./contracts"; +export * from "./deposit"; export * from "./devnet"; export * from "./expectations"; export * from "./manager"; -export * from "./multisig"; -export * from "./openZeppelinAccount"; -export * from "./outsideExecution"; +export * from "./protocol"; export * from "./receipts"; -export * from "./recovery"; export * from "./signers/legacy"; -export * from "./signers/secp256"; export * from "./signers/signers"; -export * from "./signers/webauthn"; export * from "./tokens"; -export * from "./udc"; -export * from "./upgrade"; export type Constructor = new (...args: any[]) => T; diff --git a/lib/multisig.ts b/lib/multisig.ts deleted file mode 100644 index 5855bdc..0000000 --- a/lib/multisig.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Account, CallData, Contract, GetTransactionReceiptResponse, RPC, hash, num } from "starknet"; -import { - ArgentAccount, - KeyPair, - LegacyMultisigSigner, - MultisigSigner, - deployer, - fundAccount, - fundAccountCall, - manager, - randomLegacyMultisigKeyPairs, - randomStarknetKeyPair, - randomStarknetKeyPairs, - sortByGuid, -} from "."; - -export interface MultisigWallet { - account: Account; - accountContract: Contract; - keys: KeyPair[]; - threshold: bigint; - receipt: GetTransactionReceiptResponse; -} - -export type DeployMultisigParams = { - threshold: number; - signersLength?: number; - keys?: KeyPair[]; - useTxV3?: boolean; - classHash?: string; - salt?: string; - fundingAmount?: number | bigint; - selfDeploy?: boolean; - selfDeploymentIndexes?: number[]; -}; - -export async function deployMultisig(params: DeployMultisigParams): Promise { - const finalParams = { - ...params, - classHash: params.classHash ?? (await manager.declareLocalContract("ArgentMultisigAccount")), - salt: params.salt ?? num.toHex(randomStarknetKeyPair().privateKey), - useTxV3: params.useTxV3 ?? false, - selfDeploy: params.selfDeploy ?? false, - selfDeploymentIndexes: params.selfDeploymentIndexes ?? [0], - }; - - if (params.selfDeploymentIndexes && !finalParams.selfDeploy) { - throw new Error("selfDeploymentIndexes can only be used with selfDeploy"); - } - - if (!params.keys && !finalParams.signersLength) { - throw new Error("Fill in one of 'keys' or 'signersLength'"); - } - const keys = params.keys ?? sortedKeyPairs(finalParams.signersLength!); - const signers = keysToSigners(keys); - const constructorCalldata = CallData.compile({ threshold: finalParams.threshold, signers }); - - const { classHash, salt, selfDeploymentIndexes } = finalParams; - const accountAddress = hash.calculateContractAddressFromHash(salt, classHash, constructorCalldata, 0); - - const fundingCall = finalParams.useTxV3 - ? await fundAccountCall(accountAddress, finalParams.fundingAmount ?? 1e16, "STRK") // 0.01 STRK - : await fundAccountCall(accountAddress, finalParams.fundingAmount ?? 1e15, "ETH"); // 0.001 ETH - const calls = fundingCall ? [fundingCall] : []; - - const transactionVersion = finalParams.useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; - - let transactionHash; - if (finalParams.selfDeploy) { - const response = await deployer.execute(calls); - await manager.waitForTransaction(response.transaction_hash); - - const selfDeploymentSigner = new MultisigSigner(keys.filter((_, i) => selfDeploymentIndexes.includes(i))); - const account = new Account(manager, accountAddress, selfDeploymentSigner, "1", transactionVersion); - - const { transaction_hash } = await account.deploySelf({ classHash, constructorCalldata, addressSalt: salt }); - transactionHash = transaction_hash; - } else { - const udcCalls = deployer.buildUDCContractPayload({ classHash, salt, constructorCalldata, unique: false }); - const { transaction_hash } = await deployer.execute([...calls, ...udcCalls]); - transactionHash = transaction_hash; - } - - const receipt = await manager.waitForTransaction(transactionHash); - const signer = new MultisigSigner(keys.slice(0, finalParams.threshold)); - const account = new ArgentAccount(manager, accountAddress, signer, "1", transactionVersion); - const accountContract = await manager.loadContract(account.address); - accountContract.connect(account); - return { account, accountContract, keys, receipt, threshold: BigInt(finalParams.threshold) }; -} - -export async function deployMultisig1_3( - params: Omit = {}, -): Promise { - return deployMultisig({ ...params, threshold: 1, signersLength: 3 }); -} - -export async function deployMultisig1_1( - params: Omit = {}, -): Promise { - return deployMultisig({ ...params, threshold: 1, signersLength: 1 }); -} - -const sortedKeyPairs = (length: number) => sortByGuid(randomStarknetKeyPairs(length)); - -const keysToSigners = (keys: KeyPair[]) => keys.map(({ signer }) => signer); - -export async function deployLegacyMultisig(classHash: string, threshold = 1) { - const keys = randomLegacyMultisigKeyPairs(threshold); - const signersPublicKeys = keys.map((key) => key.publicKey); - const salt = num.toHex(randomStarknetKeyPair().privateKey); - const constructorCalldata = CallData.compile({ threshold, signers: signersPublicKeys }); - const contractAddress = hash.calculateContractAddressFromHash(salt, classHash, constructorCalldata, 0); - await fundAccount(contractAddress, 1e15, "ETH"); // 0.001 ETH - const deploySigner = new LegacyMultisigSigner([keys[0]]); - const account = new Account(manager, contractAddress, deploySigner, "1"); - - const { transaction_hash } = await account.deploySelf({ classHash, constructorCalldata, addressSalt: salt }); - await manager.waitForTransaction(transaction_hash); - - const signers = new LegacyMultisigSigner(keys); - account.signer = signers; - const accountContract = await manager.loadContract(account.address); - accountContract.connect(account); - return { account, accountContract, deploySigner, signers }; -} diff --git a/lib/openZeppelinAccount.ts b/lib/openZeppelinAccount.ts deleted file mode 100644 index b47df9c..0000000 --- a/lib/openZeppelinAccount.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Account, CallData, RPC, hash, num } from "starknet"; -import { deployer, fundAccountCall } from "./accounts"; -import { ContractWithClass } from "./contracts"; -import { manager } from "./manager"; -import { LegacyMultisigSigner, LegacyStarknetKeyPair } from "./signers/legacy"; -import { randomStarknetKeyPair } from "./signers/signers"; - -export type DeployOzAccountParams = { - useTxV3?: boolean; - owner?: LegacyStarknetKeyPair; - salt?: string; - fundingAmount?: number | bigint; -}; - -export type DeployOzAccountResult = { - account: Account; - accountContract: ContractWithClass; - deployTxHash: string; - useTxV3: boolean; - owner: LegacyStarknetKeyPair; - salt: string; -}; - -export async function deployOpenZeppelinAccount(params: DeployOzAccountParams): Promise { - const classHash = await manager.declareLocalContract("AccountUpgradeable"); - const finalParams = { - ...params, - salt: params.salt ?? num.toHex(randomStarknetKeyPair().privateKey), - owner: params.owner ?? new LegacyStarknetKeyPair(), - useTxV3: params.useTxV3 ?? false, - }; - - const constructorCalldata = CallData.compile({ - owner: finalParams.owner.publicKey, - }); - - const contractAddress = hash.calculateContractAddressFromHash(finalParams.salt, classHash, constructorCalldata, 0); - - const fundingCall = finalParams.useTxV3 - ? await fundAccountCall(contractAddress, finalParams.fundingAmount ?? 1e18, "STRK") - : await fundAccountCall(contractAddress, finalParams.fundingAmount ?? 1e16, "ETH"); - const response = await deployer.execute([fundingCall!]); - await manager.waitForTransaction(response.transaction_hash); - - const defaultTxVersion = finalParams.useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; - const signer = new LegacyMultisigSigner([finalParams.owner]); - const account = new Account(manager, contractAddress, signer, "1", defaultTxVersion); - - const { transaction_hash: deployTxHash } = await account.deploySelf({ - classHash, - constructorCalldata, - addressSalt: finalParams.salt, - }); - - await manager.waitForTransaction(deployTxHash); - const accountContract = await manager.loadContract(account.address, classHash); - accountContract.connect(account); - - return { ...finalParams, account, accountContract, deployTxHash }; -} diff --git a/lib/outsideExecution.ts b/lib/outsideExecution.ts deleted file mode 100644 index 348c66a..0000000 --- a/lib/outsideExecution.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { Call, CallData, hash, num, RawArgs, SignerInterface, typedData } from "starknet"; -import { manager } from "./"; - -const typesRev0 = { - StarkNetDomain: [ - { name: "name", type: "felt" }, - { name: "version", type: "felt" }, - { name: "chainId", type: "felt" }, - ], - OutsideExecution: [ - { name: "caller", type: "felt" }, - { name: "nonce", type: "felt" }, - { name: "execute_after", type: "felt" }, - { name: "execute_before", type: "felt" }, - { name: "calls_len", type: "felt" }, - { name: "calls", type: "OutsideCall*" }, - ], - OutsideCall: [ - { name: "to", type: "felt" }, - { name: "selector", type: "felt" }, - { name: "calldata_len", type: "felt" }, - { name: "calldata", type: "felt*" }, - ], -}; - -const typesRev1 = { - StarknetDomain: [ - { name: "name", type: "shortstring" }, - { name: "version", type: "shortstring" }, - { name: "chainId", type: "shortstring" }, - { name: "revision", type: "shortstring" }, - ], - OutsideExecution: [ - { name: "Caller", type: "ContractAddress" }, - { name: "Nonce", type: "felt" }, - { name: "Execute After", type: "u128" }, - { name: "Execute Before", type: "u128" }, - { name: "Calls", type: "Call*" }, - ], - Call: [ - { name: "To", type: "ContractAddress" }, - { name: "Selector", type: "selector" }, - { name: "Calldata", type: "felt*" }, - ], -}; - -function getDomain(chainId: string, revision: typedData.TypedDataRevision) { - if (revision == typedData.TypedDataRevision.Active) { - // WARNING! Version and revision are encoded as numbers in the StarkNetDomain type and not as shortstring - // This is due to a bug in the Braavos implementation, and has been kept for compatibility - return { - name: "Account.execute_from_outside", - version: "2", - chainId: chainId, - revision: "1", - }; - } - return { - name: "Account.execute_from_outside", - version: "1", - chainId: chainId, - }; -} - -export interface OutsideExecution { - caller: string; - nonce: num.BigNumberish; - execute_after: num.BigNumberish; - execute_before: num.BigNumberish; - calls: OutsideCall[]; -} - -export interface OutsideCall { - to: string; - selector: num.BigNumberish; - calldata: RawArgs; -} - -export function getOutsideCall(call: Call): OutsideCall { - return { - to: call.contractAddress, - selector: hash.getSelectorFromName(call.entrypoint), - calldata: call.calldata ?? [], - }; -} - -export function getTypedDataHash( - outsideExecution: OutsideExecution, - accountAddress: num.BigNumberish, - chainId: string, - revision: typedData.TypedDataRevision, -): string { - return typedData.getMessageHash(getTypedData(outsideExecution, chainId, revision), accountAddress); -} - -export function getTypedData( - outsideExecution: OutsideExecution, - chainId: string, - revision: typedData.TypedDataRevision, -) { - if (revision == typedData.TypedDataRevision.Active) { - return { - types: typesRev1, - primaryType: "OutsideExecution", - domain: getDomain(chainId, revision), - message: { - Caller: outsideExecution.caller, - Nonce: outsideExecution.nonce, - "Execute After": outsideExecution.execute_after, - "Execute Before": outsideExecution.execute_before, - Calls: outsideExecution.calls.map((call) => { - return { - To: call.to, - Selector: call.selector, - Calldata: call.calldata, - }; - }), - }, - }; - } - - return { - types: typesRev0, - primaryType: "OutsideExecution", - domain: getDomain(chainId, revision), - message: { - ...outsideExecution, - calls_len: outsideExecution.calls.length, - calls: outsideExecution.calls.map((call) => { - return { - ...call, - calldata_len: call.calldata.length, - calldata: call.calldata, - }; - }), - }, - }; -} - -export async function getOutsideExecutionCall( - outsideExecution: OutsideExecution, - accountAddress: string, - signer: SignerInterface, - revision: typedData.TypedDataRevision, - chainId?: string, -): Promise { - chainId = chainId ?? (await manager.getChainId()); - const currentTypedData = getTypedData(outsideExecution, chainId, revision); - const signature = await signer.signMessage(currentTypedData, accountAddress); - return { - contractAddress: accountAddress, - entrypoint: revision == typedData.TypedDataRevision.Active ? "execute_from_outside_v2" : "execute_from_outside", - calldata: CallData.compile({ ...outsideExecution, signature }), - }; -} diff --git a/lib/protocol.ts b/lib/protocol.ts new file mode 100644 index 0000000..bf9f1f5 --- /dev/null +++ b/lib/protocol.ts @@ -0,0 +1,21 @@ +import { Contract } from "starknet"; +import { deployer, manager } from "."; + +const cache: Record = {}; + +export async function setupGiftProtocol(): Promise<{ + factory: Contract; + claimAccountClassHash: string; +}> { + const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); + const cachedFactory = cache["GiftFactory"]; + if (cachedFactory) { + return { factory: cachedFactory, claimAccountClassHash }; + } + const factory = await manager.deployContract("GiftFactory", { + unique: true, + constructorCalldata: [claimAccountClassHash, deployer.address], + }); + cache["GiftFactory"] = factory; + return { factory, claimAccountClassHash }; +} diff --git a/lib/recovery.ts b/lib/recovery.ts deleted file mode 100644 index 6424df8..0000000 --- a/lib/recovery.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { expect } from "chai"; -import { CairoCustomEnum, Contract, hash } from "starknet"; -import { RawSigner } from "."; - -export const ESCAPE_SECURITY_PERIOD = 7n * 24n * 60n * 60n; // 7 days -export const ESCAPE_EXPIRY_PERIOD = 2n * 7n * 24n * 60n * 60n; // 14 days -export const MAX_U64 = 2n ** 64n - 1n; - -export enum EscapeStatus { - None, - NotReady, - Ready, - Expired, -} - -export const ESCAPE_TYPE_NONE = new CairoCustomEnum({ - None: {}, - Guardian: undefined, - Owner: undefined, -}); - -export const ESCAPE_TYPE_GUARDIAN = new CairoCustomEnum({ - None: undefined, - Guardian: {}, - Owner: undefined, -}); - -export const ESCAPE_TYPE_OWNER = new CairoCustomEnum({ - None: undefined, - Guardian: undefined, - Owner: {}, -}); - -export const signChangeOwnerMessage = async ( - accountAddress: string, - currentOwnerGuid: bigint, - newOwner: RawSigner, - chainId: string, -) => { - const messageHash = await getChangeOwnerMessageHash(accountAddress, currentOwnerGuid, chainId); - return newOwner.signRaw(messageHash); -}; - -export const getChangeOwnerMessageHash = async (accountAddress: string, currentOwnerGuid: bigint, chainId: string) => { - const changeOwnerSelector = hash.getSelectorFromName("change_owner"); - return hash.computeHashOnElements([changeOwnerSelector, chainId, accountAddress, currentOwnerGuid]); -}; - -export async function hasOngoingEscape(accountContract: Contract): Promise { - const escape = await accountContract.get_escape(); - return escape.escape_type != 0n && escape.ready_at != 0n && escape.new_signer != 0n; -} - -export async function getEscapeStatus(accountContract: Contract): Promise { - // StarknetJs parsing is broken so we do it manually - const result = (await accountContract.call("get_escape_and_status", undefined, { parseResponse: false })) as string[]; - const result_len = result.length; - expect(result_len).to.be.oneOf([4, 6]); - const status = Number(result[result_len - 1]); - expect(status).to.be.lessThan(4, `Unknown status ${status}`); - return status; -} diff --git a/lib/signers/cairo0-sha256.patch b/lib/signers/cairo0-sha256.patch deleted file mode 100644 index 4dc4e56..0000000 --- a/lib/signers/cairo0-sha256.patch +++ /dev/null @@ -1,31 +0,0 @@ -commit d77431b967d84de3a902fbfea5cf8e1ac972f6de -Author: Yoav Gaziel -Date: Thu Nov 16 11:10:28 2023 +0200 - - add external entrypoint in main.cairo - -diff --git a/src/main.cairo b/src/main.cairo -new file mode 100644 -index 0000000..16c4e00 ---- /dev/null -+++ b/src/main.cairo -@@ -0,0 +1,19 @@ -+%lang starknet -+from starkware.cairo.common.alloc import alloc -+from starkware.cairo.common.math import split_int -+from starkware.cairo.common.memcpy import memcpy -+from starkware.cairo.common.cairo_builtins import BitwiseBuiltin, HashBuiltin -+from src.sha256 import sha256, finalize_sha256 -+ -+@view -+func sha256_cairo0{bitwise_ptr: BitwiseBuiltin*, pedersen_ptr: HashBuiltin*, range_check_ptr}( -+ data_len: felt, data: felt*, data_len_no_padding: felt -+) -> (result_len: felt, result: felt*) { -+ alloc_locals; -+ let (local sha256_ptr_start: felt*) = alloc(); -+ let sha256_ptr = sha256_ptr_start; -+ let sha256_ptr_end = sha256_ptr_start; -+ let (hash) = sha256{sha256_ptr=sha256_ptr}(data, data_len_no_padding); -+ finalize_sha256(sha256_ptr_start=sha256_ptr_start, sha256_ptr_end=sha256_ptr_end); -+ return (8, hash); -+} diff --git a/lib/signers/legacy.ts b/lib/signers/legacy.ts index 37a39b1..1132ce7 100644 --- a/lib/signers/legacy.ts +++ b/lib/signers/legacy.ts @@ -20,20 +20,6 @@ export class LegacyArgentSigner extends RawSigner { } } -export class LegacyMultisigSigner extends RawSigner { - constructor(public keys: RawSigner[]) { - super(); - } - - async signRaw(messageHash: string): Promise { - const keys = []; - for (const key of this.keys) { - keys.push(await key.signRaw(messageHash)); - } - return keys.flat(); - } -} - export abstract class LegacyKeyPair extends RawSigner { abstract get privateKey(): string; abstract get publicKey(): bigint; @@ -60,28 +46,3 @@ export class LegacyStarknetKeyPair extends LegacyKeyPair { return [r.toString(), s.toString()]; } } - -export class LegacyMultisigKeyPair extends LegacyKeyPair { - pk: string; - - constructor(pk?: string | bigint) { - super(); - this.pk = pk ? `${pk}` : `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; - } - - public get publicKey() { - return BigInt(ec.starkCurve.getStarkKey(this.pk)); - } - - public get privateKey(): string { - return this.pk; - } - - public async signRaw(messageHash: string): Promise { - const { r, s } = ec.starkCurve.sign(messageHash, this.pk); - return [this.publicKey.toString(), r.toString(), s.toString()]; - } -} - -export const randomLegacyMultisigKeyPairs = (length: number) => - Array.from({ length }, () => new LegacyMultisigKeyPair()).sort((n1, n2) => (n1.publicKey < n2.publicKey ? -1 : 1)); diff --git a/lib/signers/secp256.ts b/lib/signers/secp256.ts deleted file mode 100644 index 9c52dfc..0000000 --- a/lib/signers/secp256.ts +++ /dev/null @@ -1,229 +0,0 @@ -import * as utils from "@noble/curves/abstract/utils"; -import { p256 as secp256r1 } from "@noble/curves/p256"; -import { secp256k1 } from "@noble/curves/secp256k1"; -import { Signature as EthersSignature, Wallet } from "ethers"; -import { CairoCustomEnum, CallData, hash, num, shortString, uint256 } from "starknet"; -import { KeyPair, SignerType, signerTypeToCustomEnum } from "../signers/signers"; - -export type NormalizedSecpSignature = { r: bigint; s: bigint; yParity: boolean }; - -export function normalizeSecpR1Signature(signature: { - r: bigint; - s: bigint; - recovery: number; -}): NormalizedSecpSignature { - return normalizeSecpSignature(secp256r1, signature); -} - -export function normalizeSecpK1Signature(signature: { - r: bigint; - s: bigint; - recovery: number; -}): NormalizedSecpSignature { - return normalizeSecpSignature(secp256k1, signature); -} - -export function normalizeSecpSignature( - curve: typeof secp256r1 | typeof secp256k1, - signature: { r: bigint; s: bigint; recovery: number }, -): NormalizedSecpSignature { - let s = signature.s; - let yParity = signature.recovery !== 0; - if (s > curve.CURVE.n / 2n) { - s = curve.CURVE.n - s; - yParity = !yParity; - } - return { r: signature.r, s, yParity }; -} - -export class EthKeyPair extends KeyPair { - pk: bigint; - allowLowS?: boolean; - - constructor(pk?: string | bigint, allowLowS?: boolean) { - super(); - - if (pk == undefined) { - pk = Wallet.createRandom().privateKey; - } - if (typeof pk === "string") { - pk = BigInt(pk); - } - this.pk = pk; - this.allowLowS = allowLowS; - } - - public get address(): bigint { - return BigInt(new Wallet("0x" + padTo32Bytes(num.toHex(this.pk))).address); - } - - public get guid(): bigint { - return BigInt(hash.computePoseidonHash(shortString.encodeShortString("Secp256k1 Signer"), this.address)); - } - - public get storedValue(): bigint { - throw new Error("Not implemented yet"); - } - - public get signer(): CairoCustomEnum { - return signerTypeToCustomEnum(SignerType.Secp256k1, { signer: this.address }); - } - - public async signRaw(messageHash: string): Promise { - const signature = normalizeSecpK1Signature( - secp256k1.sign(padTo32Bytes(messageHash), this.pk, { lowS: this.allowLowS }), - ); - - return CallData.compile([ - signerTypeToCustomEnum(SignerType.Secp256k1, { - pubkeyHash: this.address, - r: uint256.bnToUint256(signature.r), - s: uint256.bnToUint256(signature.s), - y_parity: signature.yParity, - }), - ]); - } -} - -export class Eip191KeyPair extends KeyPair { - pk: string; - - constructor(pk?: string | bigint) { - super(); - this.pk = pk ? "0x" + padTo32Bytes(num.toHex(pk)) : Wallet.createRandom().privateKey; - } - - public get address() { - return BigInt(new Wallet(this.pk).address); - } - - public get guid(): bigint { - return BigInt(hash.computePoseidonHash(shortString.encodeShortString("Eip191 Signer"), this.address)); - } - - public get storedValue(): bigint { - throw new Error("Not implemented yet"); - } - - public get signer(): CairoCustomEnum { - return signerTypeToCustomEnum(SignerType.Eip191, { signer: this.address }); - } - - public async signRaw(messageHash: string): Promise { - const ethSigner = new Wallet(this.pk); - messageHash = "0x" + padTo32Bytes(messageHash); - const ethersSignature = EthersSignature.from(ethSigner.signMessageSync(num.hexToBytes(messageHash))); - - const signature = normalizeSecpK1Signature({ - r: BigInt(ethersSignature.r), - s: BigInt(ethersSignature.s), - recovery: ethersSignature.yParity ? 1 : 0, - }); - - return CallData.compile([ - signerTypeToCustomEnum(SignerType.Eip191, { - ethAddress: this.address, - r: uint256.bnToUint256(signature.r), - s: uint256.bnToUint256(signature.s), - y_parity: signature.yParity, - }), - ]); - } -} - -export class EstimateEip191KeyPair extends KeyPair { - readonly address: bigint; - - constructor(address: bigint) { - super(); - this.address = address; - } - - public get privateKey(): string { - throw new Error("EstimateEip191KeyPair does not have a private key"); - } - - public get guid(): bigint { - throw new Error("Not implemented yet"); - } - - public get storedValue(): bigint { - throw new Error("Not implemented yet"); - } - - public get signer(): CairoCustomEnum { - return signerTypeToCustomEnum(SignerType.Eip191, { signer: this.address }); - } - - public async signRaw(messageHash: string): Promise { - return CallData.compile([ - signerTypeToCustomEnum(SignerType.Eip191, { - ethAddress: this.address, - r: uint256.bnToUint256("0x1556a70d76cc452ae54e83bb167a9041f0d062d000fa0dcb42593f77c544f647"), - s: uint256.bnToUint256("0x1643d14dbd6a6edc658f4b16699a585181a08dba4f6d16a9273e0e2cbed622da"), - y_parity: false, - }), - ]); - } -} - -export class Secp256r1KeyPair extends KeyPair { - pk: bigint; - private allowLowS?: boolean; - - constructor(pk?: string | bigint, allowLowS?: boolean) { - super(); - this.pk = BigInt(pk ? `${pk}` : Wallet.createRandom().privateKey); - this.allowLowS = allowLowS; - } - - public get publicKey() { - const publicKey = secp256r1.getPublicKey(this.pk).slice(1); - return uint256.bnToUint256("0x" + utils.bytesToHex(publicKey)); - } - - public get guid(): bigint { - return BigInt( - hash.computePoseidonHashOnElements([ - shortString.encodeShortString("Secp256r1 Signer"), - this.publicKey.low, - this.publicKey.high, - ]), - ); - } - - public get storedValue(): bigint { - throw new Error("Not implemented yet"); - } - - public get signer() { - return signerTypeToCustomEnum(SignerType.Secp256r1, { signer: this.publicKey }); - } - - public async signRaw(messageHash: string): Promise { - messageHash = padTo32Bytes(messageHash); - const signature = normalizeSecpR1Signature(secp256r1.sign(messageHash, this.pk, { lowS: this.allowLowS })); - return CallData.compile([ - signerTypeToCustomEnum(SignerType.Secp256r1, { - pubkey: this.publicKey, - r: uint256.bnToUint256(signature.r), - s: uint256.bnToUint256(signature.s), - y_parity: signature.yParity, - }), - ]); - } -} - -export function padTo32Bytes(hexString: string): string { - if (hexString.startsWith("0x")) { - hexString = hexString.slice(2); - } - if (hexString.length < 64) { - hexString = "0".repeat(64 - hexString.length) + hexString; - } - return hexString; -} - -export const randomEthKeyPair = () => new EthKeyPair(); -export const randomEip191KeyPair = () => new Eip191KeyPair(); -export const randomSecp256r1KeyPair = () => new Secp256r1KeyPair(); diff --git a/lib/signers/signers.ts b/lib/signers/signers.ts index 0f3c956..a6f4cd4 100644 --- a/lib/signers/signers.ts +++ b/lib/signers/signers.ts @@ -1,10 +1,6 @@ import { - CairoCustomEnum, - CairoOption, - CairoOptionVariant, Call, CallData, - Calldata, DeclareSignerDetails, DeployAccountSignerDetails, InvocationsSignerDetails, @@ -17,11 +13,7 @@ import { V3DeclareSignerDetails, V3DeployAccountSignerDetails, V3InvocationsSignerDetails, - ec, - encode, hash, - num, - shortString, stark, transaction, typedData, @@ -129,174 +121,3 @@ export abstract class RawSigner implements SignerInterface { return await this.signRaw(msgHash); } } - -export class MultisigSigner extends RawSigner { - constructor(public keys: KeyPair[]) { - super(); - } - - async signRaw(messageHash: string): Promise { - const keys = []; - for (const key of this.keys) { - keys.push(await key.signRaw(messageHash)); - } - return [keys.length.toString(), keys.flat()].flat(); - } -} - -export class ArgentSigner extends MultisigSigner { - constructor( - public owner: KeyPair = randomStarknetKeyPair(), - public guardian?: KeyPair, - ) { - const signers = [owner]; - if (guardian) { - signers.push(guardian); - } - super(signers); - } -} - -export abstract class KeyPair extends RawSigner { - abstract get signer(): CairoCustomEnum; - abstract get guid(): bigint; - abstract get storedValue(): bigint; - - public get compiledSigner(): Calldata { - return CallData.compile([this.signer]); - } - - public get signerAsOption() { - return new CairoOption(CairoOptionVariant.Some, { - signer: this.signer, - }); - } - public get compiledSignerAsOption() { - return CallData.compile([this.signerAsOption]); - } -} - -export class StarknetKeyPair extends KeyPair { - pk: string; - - constructor(pk?: string | bigint) { - super(); - this.pk = pk ? num.toHex(pk) : `0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`; - } - - public get privateKey(): string { - return this.pk; - } - - public get publicKey() { - return BigInt(ec.starkCurve.getStarkKey(this.pk)); - } - - public get guid() { - return BigInt(hash.computePoseidonHash(shortString.encodeShortString("Starknet Signer"), this.publicKey)); - } - - public get storedValue() { - return this.publicKey; - } - - public get signer(): CairoCustomEnum { - return signerTypeToCustomEnum(SignerType.Starknet, { signer: this.publicKey }); - } - - public async signRaw(messageHash: string): Promise { - const { r, s } = ec.starkCurve.sign(messageHash, this.pk); - return starknetSignatureType(this.publicKey, r, s); - } -} - -export class EstimateStarknetKeyPair extends KeyPair { - readonly pubKey: bigint; - - constructor(pubKey: bigint) { - super(); - this.pubKey = pubKey; - } - - public get privateKey(): string { - throw new Error("EstimateStarknetKeyPair does not have a private key"); - } - - public get publicKey() { - return this.pubKey; - } - - public get guid() { - return BigInt(hash.computePoseidonHash(shortString.encodeShortString("Starknet Signer"), this.publicKey)); - } - - public get storedValue() { - return this.publicKey; - } - - public get signer(): CairoCustomEnum { - return signerTypeToCustomEnum(SignerType.Starknet, { signer: this.publicKey }); - } - - public async signRaw(messageHash: string): Promise { - const fakeR = "0x6cefb49a1f4eb406e8112db9b8cdf247965852ddc5ca4d74b09e42471689495"; - const fakeS = "0x25760910405a052b7f08ec533939c54948bc530c662c5d79e8ff416579087f7"; - return starknetSignatureType(this.publicKey, fakeR, fakeS); - } -} - -export function starknetSignatureType( - signer: bigint | number | string, - r: bigint | number | string, - s: bigint | number | string, -) { - return CallData.compile([signerTypeToCustomEnum(SignerType.Starknet, { signer, r, s })]); -} - -export function zeroStarknetSignatureType() { - return signerTypeToCustomEnum(SignerType.Starknet, { signer: 0 }); -} - -// reflects the signer type in signer_signature.cairo -// needs to be updated for the signer types -// used to convert signertype to guid -export enum SignerType { - Starknet, - Secp256k1, - Secp256r1, - Eip191, - Webauthn, -} - -export function signerTypeToCustomEnum(signerType: SignerType, value: any): CairoCustomEnum { - const contents = { - Starknet: undefined, - Secp256k1: undefined, - Secp256r1: undefined, - Eip191: undefined, - Webauthn: undefined, - }; - - if (signerType === SignerType.Starknet) { - contents.Starknet = value; - } else if (signerType === SignerType.Secp256k1) { - contents.Secp256k1 = value; - } else if (signerType === SignerType.Secp256r1) { - contents.Secp256r1 = value; - } else if (signerType === SignerType.Eip191) { - contents.Eip191 = value; - } else if (signerType === SignerType.Webauthn) { - contents.Webauthn = value; - } else { - throw new Error(`Unknown SignerType`); - } - - return new CairoCustomEnum(contents); -} - -export function sortByGuid(keys: KeyPair[]) { - return keys.sort((n1, n2) => (n1.guid < n2.guid ? -1 : 1)); -} - -export const randomStarknetKeyPair = () => new StarknetKeyPair(); -export const randomStarknetKeyPairs = (length: number) => Array.from({ length }, randomStarknetKeyPair); diff --git a/lib/signers/webauthn.ts b/lib/signers/webauthn.ts deleted file mode 100644 index f38a1de..0000000 --- a/lib/signers/webauthn.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { concatBytes } from "@noble/curves/abstract/utils"; -import { p256 as secp256r1 } from "@noble/curves/p256"; -import { BinaryLike, createHash } from "crypto"; -import { - ArraySignatureType, - BigNumberish, - CairoCustomEnum, - CallData, - Uint256, - hash, - shortString, - uint256, -} from "starknet"; -import { KeyPair, SignerType, normalizeSecpR1Signature, signerTypeToCustomEnum } from ".."; - -const buf2hex = (buffer: ArrayBuffer, prefix = true) => - `${prefix ? "0x" : ""}${[...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, "0")).join("")}`; - -const normalizeTransactionHash = (transactionHash: string) => transactionHash.replace(/^0x/, "").padStart(64, "0"); - -const buf2base64url = (buffer: ArrayBuffer) => - buf2base64(buffer).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); - -const buf2base64 = (buffer: ArrayBuffer) => btoa(String.fromCharCode(...new Uint8Array(buffer))); - -const hex2buf = (hex: string) => - Uint8Array.from( - hex - .replace(/^0x/, "") - .match(/.{1,2}/g)! - .map((byte) => parseInt(byte, 16)), - ); - -const toCharArray = (value: string) => CallData.compile(value.split("").map(shortString.encodeShortString)); - -interface WebauthnSigner { - origin: BigNumberish[]; - rp_id_hash: Uint256; - pubkey: Uint256; -} - -interface WebauthnSignature { - cross_origin: boolean; - client_data_json_outro: BigNumberish[]; - flags: number; - sign_count: number; - ec_signature: { r: Uint256; s: Uint256; y_parity: boolean }; - sha256_implementation: CairoCustomEnum; -} - -export class WebauthnOwner extends KeyPair { - pk: Uint8Array; - rpIdHash: Uint256; - - constructor( - pk?: string, - public rpId = "localhost", - public origin = "http://localhost:5173", - ) { - super(); - this.pk = pk ? hex2buf(normalizeTransactionHash(pk)) : secp256r1.utils.randomPrivateKey(); - this.rpIdHash = uint256.bnToUint256(buf2hex(sha256(rpId))); - } - - public get publicKey() { - return secp256r1.getPublicKey(this.pk).slice(1); - } - - public get guid(): bigint { - const rpIdHashAsU256 = this.rpIdHash; - const publicKeyAsU256 = uint256.bnToUint256(buf2hex(this.publicKey)); - const originBytes = toCharArray(this.origin); - const elements = [ - shortString.encodeShortString("Webauthn Signer"), - originBytes.length, - ...originBytes, - rpIdHashAsU256.low, - rpIdHashAsU256.high, - publicKeyAsU256.low, - publicKeyAsU256.high, - ]; - return BigInt(hash.computePoseidonHashOnElements(elements)); - } - - public get storedValue(): bigint { - throw new Error("Not implemented yet"); - } - - public get signer(): CairoCustomEnum { - const signer: WebauthnSigner = { - origin: toCharArray(this.origin), - rp_id_hash: this.rpIdHash, - pubkey: uint256.bnToUint256(buf2hex(this.publicKey)), - }; - return signerTypeToCustomEnum(SignerType.Webauthn, signer); - } - - public async signRaw(messageHash: string): Promise { - const webauthnSigner = this.signer.variant.Webauthn; - const webauthnSignature = await this.signHash(messageHash); - return CallData.compile([signerTypeToCustomEnum(SignerType.Webauthn, { webauthnSigner, webauthnSignature })]); - } - - public async signHash(transactionHash: string): Promise { - const flags = "0b00000101"; // present and verified - const signCount = 0; - const authenticatorData = concatBytes(sha256(this.rpId), new Uint8Array([Number(flags), 0, 0, 0, signCount])); - - const sha256Impl = 0; - const challenge = buf2base64url(hex2buf(`${normalizeTransactionHash(transactionHash)}0${sha256Impl}`)); - const crossOrigin = false; - const extraJson = ""; // = `,"extraField":"random data"}`; - const clientData = JSON.stringify({ type: "webauthn.get", challenge, origin: this.origin, crossOrigin }); - const clientDataJson = extraJson ? clientData.replace(/}$/, extraJson) : clientData; - const clientDataHash = sha256(new TextEncoder().encode(clientDataJson)); - - const signedHash = sha256(concatBytes(authenticatorData, clientDataHash)); - - const signature = normalizeSecpR1Signature(secp256r1.sign(signedHash, this.pk)); - - // console.log(` - // let transaction_hash = ${transactionHash}; - // let pubkey = ${buf2hex(this.publicKey)}; - // let signer = new_webauthn_signer(:origin, :rp_id_hash, :pubkey); - // let signature = WebauthnSignature { - // cross_origin: ${crossOrigin}, - // client_data_json_outro: ${extraJson ? `${JSON.stringify(extraJson)}.into_bytes()` : "array![]"}.span(), - // flags: ${flags}, - // sign_count: ${signCount}, - // ec_signature: Signature { - // r: 0x${r.toString(16)}, - // s: 0x${s.toString(16)}, - // y_parity: ${recovery !== 0}, - // }, - // sha256_implementation: Sha256Implementation::Cairo${sha256Impl}, - // };`); - - return { - cross_origin: crossOrigin, - client_data_json_outro: CallData.compile(toCharArray(extraJson)), - flags: Number(flags), - sign_count: signCount, - ec_signature: { - r: uint256.bnToUint256(signature.r), - s: uint256.bnToUint256(signature.s), - y_parity: signature.yParity, - }, - sha256_implementation: new CairoCustomEnum({ - Cairo0: sha256Impl ? undefined : {}, - Cairo1: sha256Impl ? {} : undefined, - }), - }; - } -} - -function sha256(message: BinaryLike) { - return createHash("sha256").update(message).digest(); -} - -export const randomWebauthnOwner = () => new WebauthnOwner(); diff --git a/lib/udc.ts b/lib/udc.ts deleted file mode 100644 index fe8abe1..0000000 --- a/lib/udc.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { CallData, RawCalldata } from "starknet"; -import { deployer, manager } from "."; - -export const udcAddress = "0x041a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf"; - -export async function deployContractUDC(classHash: string, salt: string, calldata: RawCalldata) { - const unique = 0n; //false - - const udcContract = await manager.loadContract(udcAddress); - - udcContract.connect(deployer); - - const deployCall = udcContract.populate("deployContract", CallData.compile([classHash, salt, unique, calldata])); - const { transaction_hash } = await udcContract.deployContract(deployCall.calldata); - - const transaction_response = await manager.waitForTransaction(transaction_hash); - - return transaction_response.events?.[0].from_address; -} diff --git a/lib/upgrade.ts b/lib/upgrade.ts deleted file mode 100644 index d60be57..0000000 --- a/lib/upgrade.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Call, CallData } from "starknet"; -import { getOutsideCall } from "./outsideExecution"; - -export function getUpgradeData(calls: Call[]) { - const externalCalls = calls.map(getOutsideCall); - return CallData.compile({ externalCalls }); -} - -export function getUpgradeDataLegacy(calls: Call[]) { - const upgradeData = getUpgradeData(calls); - return CallData.compile({ upgradeData }); -} diff --git a/tests-integration/account.test.ts b/tests-integration/account.test.ts index 6637c83..1594fa6 100644 --- a/tests-integration/account.test.ts +++ b/tests-integration/account.test.ts @@ -1,215 +1,169 @@ import { expect } from "chai"; -import { Account, CallData, RPC, hash, num, uint256 } from "starknet"; -import { LegacyStarknetKeyPair, deployer, expectRevertWithErrorMessage, manager } from "../lib"; +import { Account, RPC, num } from "starknet"; +import { + GIFT_MAX_FEE, + buildCallDataClaim, + calculateClaimAddress, + claimInternal, + defaultDepositTestSetup, + deployer, + expectRevertWithErrorMessage, + manager, + randomReceiver, + setupGiftProtocol, +} from "../lib"; describe("Gifting", function () { - const signer = new LegacyStarknetKeyPair(); - const claimPubkey = signer.publicKey; - const amount = 1000000000000000n; - const maxFee = 50000000000000n; - const receiver = "0x42"; - for (const useTxV3 of [false, true]) { it(`Testing simple claim flow using txV3: ${useTxV3}`, async function () { - await manager.restartDevnetAndClearClassCache(); - // Deploy factory - const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); - const factory = await manager.deployContract("GiftFactory", { - unique: true, - constructorCalldata: [claimAccountClassHash, deployer.address], - }); - - // Make a gift - const tokenContract = await manager.tokens.feeTokenContract(useTxV3); - tokenContract.connect(deployer); - factory.connect(deployer); - await tokenContract.approve(factory.address, amount + maxFee); - await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); - - // Ensure there is a contract for the claim - const claimAddress = await factory.get_claim_address( - claimAccountClassHash, - deployer.address, - amount, - maxFee, - tokenContract.address, - claimPubkey, - ); - - const constructorArgs = { - sender: deployer.address, - amount: uint256.bnToUint256(amount), - max_fee: maxFee, - token: tokenContract.address, - claim_pubkey: claimPubkey, - }; - const claim = { - factory: factory.address, - class_hash: claimAccountClassHash, - ...constructorArgs, - }; - - const correctAddress = hash.calculateContractAddressFromHash( - 0, - claimAccountClassHash, - CallData.compile(constructorArgs), - factory.address, - ); - expect(claimAddress).to.be.equal(num.toBigInt(correctAddress)); - - // Check balance of the claim contract is correct - await tokenContract.balance_of(claimAddress).should.eventually.equal(amount + maxFee); - // Check balance receiver address == 0 - await tokenContract.balance_of(receiver).should.eventually.equal(0n); - - const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; - const claimAccount = new Account(manager, num.toHex(claimAddress), signer, undefined, txVersion); - factory.connect(claimAccount); + const { factory } = await setupGiftProtocol(); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory, useTxV3); + const receiver = randomReceiver(); + const claimAddress = calculateClaimAddress(claim); + + await claimInternal(claim, receiver, claimPrivateKey); + + const token = await manager.loadContract(claim.gift_token); + const finalBalance = await token.balance_of(claimAddress); + expect(finalBalance < claim.fee_amount).to.be.true; + await token.balance_of(receiver).should.eventually.equal(claim.gift_amount); + }); + + it(`Test max fee too high using txV3: ${useTxV3}`, async function () { + const { factory } = await setupGiftProtocol(); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory, useTxV3); + const receiver = randomReceiver(); if (useTxV3) { - const estimate = await factory.estimateFee.claim_internal(claim, receiver); const newResourceBounds = { - ...estimate.resourceBounds, l2_gas: { - ...estimate.resourceBounds.l2_gas, - max_amount: maxFee + 1n, - max_price_per_unit: num.toHexString(4), + max_amount: num.toHexString(GIFT_MAX_FEE), + max_price_per_unit: num.toHexString(1), + }, + l1_gas: { + max_amount: num.toHexString(10), + max_price_per_unit: num.toHexString(36000000000n), // Current devnet gas price }, }; await expectRevertWithErrorMessage("gift-acc/max-fee-too-high-v3", () => - claimAccount.execute( - [ - { - contractAddress: factory.address, - calldata: [claim, receiver], - entrypoint: "claim_internal", - }, - ], - undefined, - { resourceBounds: newResourceBounds, tip: 1 }, - ), + claimInternal(claim, receiver, claimPrivateKey, { resourceBounds: newResourceBounds, tip: 1 }), ); } else { await expectRevertWithErrorMessage("gift-acc/max-fee-too-high-v1", () => - factory.claim_internal(claim, receiver, { maxFee: maxFee + 1n }), + claimInternal(claim, receiver, claimPrivateKey, { + maxFee: GIFT_MAX_FEE + 1n, + }), ); } - await factory.claim_internal(claim, receiver); - - // Final check - const finalBalance = await tokenContract.balance_of(claimAddress); - expect(finalBalance < maxFee).to.be.true; - await tokenContract.balance_of(receiver).should.eventually.equal(amount); }); } - it(`Test basic validation asserts`, async function () { - await manager.restartDevnetAndClearClassCache(); - // Deploy factory - const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); - const factory = await manager.deployContract("GiftFactory", { - unique: true, - constructorCalldata: [claimAccountClassHash, deployer.address], - }); + it(`Test only protocol can call claim contract`, async function () { + const { factory } = await setupGiftProtocol(); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + + const claimAddress = calculateClaimAddress(claim); + + const claimAccount = new Account( + manager, + num.toHex(claimAddress), + claimPrivateKey, + undefined, + RPC.ETransactionVersion.V2, + ); + const claimContract = await manager.loadContract(claimAddress); + claimContract.connect(claimAccount); + await expectRevertWithErrorMessage("gift-acc/only-protocol", () => claimContract.__validate__([])); + }); + + it(`Test claim contract cant call another contract`, async function () { + const { factory, claimAccountClassHash } = await setupGiftProtocol(); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const receiver = randomReceiver(); const fakeFactory = await manager.deployContract("GiftFactory", { unique: true, constructorCalldata: [claimAccountClassHash, deployer.address], }); - // Make a gift - const tokenContract = await manager.tokens.feeTokenContract(false); - tokenContract.connect(deployer); - factory.connect(deployer); - await tokenContract.approve(factory.address, amount + maxFee); - await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); - - // Ensure there is a contract for the claim - const claimAddress = await factory.get_claim_address( - claimAccountClassHash, - deployer.address, - amount, - maxFee, - tokenContract.address, - claimPubkey, - ); + const claimAddress = calculateClaimAddress(claim); - const constructorArgs = { - sender: deployer.address, - amount: uint256.bnToUint256(amount), - max_fee: maxFee, - token: tokenContract.address, - claim_pubkey: claimPubkey, - }; - - const constructorCalldata = CallData.compile(constructorArgs); - const correctAddress = hash.calculateContractAddressFromHash( - 0, - claimAccountClassHash, - constructorCalldata, - factory.address, + const claimAccount = new Account( + manager, + num.toHex(claimAddress), + claimPrivateKey, + undefined, + RPC.ETransactionVersion.V2, ); - expect(claimAddress).to.be.equal(num.toBigInt(correctAddress)); + fakeFactory.connect(claimAccount); - // Check balance of the claim contract is correct - await tokenContract.balance_of(claimAddress).should.eventually.equal(amount + maxFee); - // Check balance receiver address == 0 - await tokenContract.balance_of(receiver).should.eventually.equal(0n); + await expectRevertWithErrorMessage("gift-acc/invalid-call-to", () => + fakeFactory.claim_internal(buildCallDataClaim(claim), receiver, { maxFee: 400000000000000n }), + ); + }); - const claimContract = await manager.loadContract(num.toHex(claimAddress)); - const claimAccount = new Account(manager, claimContract.address, signer, undefined, RPC.ETransactionVersion.V2); - // only protocol - claimContract.connect(claimAccount); - await expectRevertWithErrorMessage("gift-acc/only-protocol", () => claimContract.__validate__([])); + it(`Test claim contract can only call 'claim_internal'`, async function () { + const { factory } = await setupGiftProtocol(); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const receiver = randomReceiver(); - const claim = { - factory: factory.address, - class_hash: claimAccountClassHash, - ...constructorArgs, - }; + const claimAddress = calculateClaimAddress(claim); - // cant call another contract - fakeFactory.connect(claimAccount); - await expectRevertWithErrorMessage("gift-acc/invalid-call-to", () => - fakeFactory.claim_internal(claim, receiver, { maxFee: 400000000000000n }), + const claimAccount = new Account( + manager, + num.toHex(claimAddress), + claimPrivateKey, + undefined, + RPC.ETransactionVersion.V2, ); - // wrong selector factory.connect(claimAccount); - await expectRevertWithErrorMessage("gift-acc/invalid-call-selector", () => factory.get_latest_claim_class_hash()); + await expectRevertWithErrorMessage("gift-acc/invalid-call-selector", () => + factory.get_dust(claim, receiver, { maxFee: 400000000000000n }), + ); + }); + + it(`Test claim contract cant preform a multicall`, async function () { + const { factory } = await setupGiftProtocol(); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const receiver = randomReceiver(); + + const claimAddress = calculateClaimAddress(claim); + + const claimAccount = new Account( + manager, + num.toHex(claimAddress), + claimPrivateKey, + undefined, + RPC.ETransactionVersion.V2, + ); - // multicall await expectRevertWithErrorMessage("gift-acc/invalid-call-len", () => claimAccount.execute([ { contractAddress: factory.address, - calldata: [claim, receiver], + calldata: [buildCallDataClaim(claim), receiver], entrypoint: "claim_internal", }, { contractAddress: factory.address, - calldata: [claim, receiver], + calldata: [buildCallDataClaim(claim), receiver], entrypoint: "claim_internal", }, ]), ); + }); + + it(`Test cannot call 'claim_internal' twice`, async function () { + const { factory } = await setupGiftProtocol(); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const receiver = randomReceiver(); // double claim - await factory.claim_internal(claim, receiver); + await claimInternal(claim, receiver, claimPrivateKey); await expectRevertWithErrorMessage("gift-acc/invalid-claim-nonce", () => - claimAccount.execute( - [ - { - contractAddress: factory.address, - calldata: [claim, receiver], - entrypoint: "claim_internal", - }, - ], - undefined, - { skipValidate: false }, - ), + claimInternal(claim, receiver, claimPrivateKey, { skipValidate: false }), ); }); - // TODO Tests: // - claim_external // - check with wrong claim data diff --git a/tests-integration/claim_external.test.ts b/tests-integration/claim_external.test.ts index f8c67d7..200b235 100644 --- a/tests-integration/claim_external.test.ts +++ b/tests-integration/claim_external.test.ts @@ -1,51 +1,13 @@ -import { uint256 } from "starknet"; -import { LegacyStarknetKeyPair, deployer, getClaimExternalData, manager } from "../lib"; +import { claimExternal, defaultDepositTestSetup, randomReceiver, setupGiftProtocol } from "../lib"; describe("claim_external", function () { - const useTxV3 = true; - it(`Testing claim_external flow using txV3: ${useTxV3}`, async function () { - await manager.restartDevnetAndClearClassCache(); - // Deploy factory - const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); - const factory = await manager.deployContract("GiftFactory", { - unique: true, - constructorCalldata: [claimAccountClassHash, deployer.address], - }); - const signer = new LegacyStarknetKeyPair(); - const claimPubkey = signer.publicKey; - const amount = 1000000000000000n; - const maxFee = 50000000000000n; - const receiver = "0x42"; - - // Make a gift - const tokenContract = await manager.tokens.feeTokenContract(useTxV3); - tokenContract.connect(deployer); - factory.connect(deployer); - await tokenContract.approve(factory.address, amount + maxFee); - await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); - - const claimAddress = await factory.get_claim_address( - claimAccountClassHash, - deployer.address, - amount, - maxFee, - tokenContract.address, - claimPubkey, - ); + for (const useTxV3 of [false, true]) { + it(`Testing claim_external flow using txV3: ${useTxV3}`, async function () { + const { factory } = await setupGiftProtocol(); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const receiver = randomReceiver(); - const claim = { - factory: factory.address, - class_hash: claimAccountClassHash, - sender: deployer.address, - amount: uint256.bnToUint256(amount), - max_fee: maxFee, - token: tokenContract.address, - claim_pubkey: claimPubkey, - }; - - const claimExternalData = await getClaimExternalData({ receiver }); - const signature = await signer.signMessage(claimExternalData, claimAddress); - - await factory.claim_external(claim, receiver, signature); - }); + await claimExternal(claim, receiver, claimPrivateKey); + }); + } }); diff --git a/tests-integration/factory.test.ts b/tests-integration/factory.test.ts index 7dbddad..3a17e8d 100644 --- a/tests-integration/factory.test.ts +++ b/tests-integration/factory.test.ts @@ -1,205 +1,117 @@ import { expect } from "chai"; -import { Account, RPC, num, uint256 } from "starknet"; -import { LegacyStarknetKeyPair, deployer, expectRevertWithErrorMessage, genericAccount, manager } from "../lib"; +import { ec, encode, num } from "starknet"; +import { + GIFT_AMOUNT, + GIFT_MAX_FEE, + LegacyStarknetKeyPair, + calculateClaimAddress, + claimInternal, + defaultDepositTestSetup, + deployer, + expectRevertWithErrorMessage, + genericAccount, + manager, + randomReceiver, + setupGiftProtocol, +} from "../lib"; describe("Factory", function () { + it(`Test calculate claim address`, async function () { + const { factory } = await setupGiftProtocol(); + const { claim } = await defaultDepositTestSetup(factory); + + const claimAddress = await factory.get_claim_address( + claim.class_hash, + deployer.address, + claim.gift_token, + claim.gift_amount, + claim.fee_token, + claim.fee_amount, + claim.claim_pubkey, + ); + + const correctAddress = calculateClaimAddress(claim); + expect(claimAddress).to.be.equal(num.toBigInt(correctAddress)); + }); for (const useTxV3 of [false, true]) { it(`get_dust: ${useTxV3}`, async function () { - await manager.restartDevnetAndClearClassCache(); - // Deploy factory - const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); - const factory = await manager.deployContract("GiftFactory", { - unique: true, - constructorCalldata: [claimAccountClassHash, deployer.address], - }); - const signer = new LegacyStarknetKeyPair(); - const claimPubkey = signer.publicKey; - const amount = 1000000000000000n; - const maxFee = 50000000000000n; - const receiver = "0x42"; - const receiverDust = "0x43"; - - // Make a gift - const tokenContract = await manager.tokens.feeTokenContract(useTxV3); - tokenContract.connect(deployer); - factory.connect(deployer); - await tokenContract.approve(factory.address, amount + maxFee); - await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); - - // Ensure there is a contract for the claim - const claimAddress = await factory.get_claim_address( - claimAccountClassHash, - deployer.address, - amount, - maxFee, - tokenContract.address, - claimPubkey, - ); - - const claim = { - factory: factory.address, - class_hash: claimAccountClassHash, - sender: deployer.address, - amount: uint256.bnToUint256(amount), - max_fee: maxFee, - token: tokenContract.address, - claim_pubkey: claimPubkey, - }; - - // Check balance of the claim contract is correct - await tokenContract.balance_of(claimAddress).should.eventually.equal(amount + maxFee); - // Check balance receiver address == 0 - await tokenContract.balance_of(receiver).should.eventually.equal(0n); - - const claimContract = await manager.loadContract(num.toHex(claimAddress)); - const txVersion = useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2; - const claimAccount = new Account(manager, claimContract.address, signer, undefined, txVersion); - factory.connect(claimAccount); - await factory.claim_internal(claim, receiver); + const { factory } = await setupGiftProtocol(); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const receiver = randomReceiver(); + const receiverDust = randomReceiver(); + + await claimInternal(claim, receiver, claimPrivateKey); + const claimAddress = calculateClaimAddress(claim); + const token = await manager.loadContract(claim.gift_token); // Final check - const dustBalance = await tokenContract.balance_of(claimAddress); - expect(dustBalance < maxFee).to.be.true; - await tokenContract.balance_of(receiver).should.eventually.equal(amount); + + const dustBalance = await token.balance_of(claimAddress); + expect(dustBalance < GIFT_MAX_FEE).to.be.true; + await token.balance_of(receiver).should.eventually.equal(GIFT_AMOUNT); // Test dust - await tokenContract.balance_of(receiverDust).should.eventually.equal(0n); + await token.balance_of(receiverDust).should.eventually.equal(0n); factory.connect(deployer); await factory.get_dust(claim, receiverDust); - await tokenContract.balance_of(claimAccount.address).should.eventually.equal(0n); - await tokenContract.balance_of(receiverDust).should.eventually.equal(dustBalance); + await token.balance_of(claimAddress).should.eventually.equal(0n); + await token.balance_of(receiverDust).should.eventually.equal(dustBalance); }); } it(`Test Cancel Claim`, async function () { - await manager.restartDevnetAndClearClassCache(); - // Deploy factory - const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); - const factory = await manager.deployContract("GiftFactory", { - unique: true, - constructorCalldata: [claimAccountClassHash, deployer.address], - }); - const signer = new LegacyStarknetKeyPair(); - const claimPubkey = signer.publicKey; - const amount = 1000000000000000n; - const maxFee = 50000000000000n; - const receiver = "0x42"; + const { factory } = await setupGiftProtocol(); + const { claim, claimPrivateKey } = await defaultDepositTestSetup(factory); + const receiver = randomReceiver(); + const token = await manager.loadContract(claim.gift_token); + const claimAddress = calculateClaimAddress(claim); - // Make a gift - const tokenContract = await manager.tokens.feeTokenContract(false); - tokenContract.connect(deployer); + const balanceSenderBefore = await token.balance_of(deployer.address); factory.connect(deployer); - await tokenContract.approve(factory.address, amount + maxFee); - await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); - - // Ensure there is a contract for the claim - const claimAddress = await factory.get_claim_address( - claimAccountClassHash, - deployer.address, - amount, - maxFee, - tokenContract.address, - claimPubkey, - ); - - const claim = { - factory: factory.address, - class_hash: claimAccountClassHash, - sender: deployer.address, - amount: uint256.bnToUint256(amount), - max_fee: maxFee, - token: tokenContract.address, - claim_pubkey: claimPubkey, - }; - - // Check balance of the claim contract is correct - await tokenContract.balance_of(claimAddress).should.eventually.equal(amount + maxFee); - // Check balance receiver address == 0 - await tokenContract.balance_of(receiver).should.eventually.equal(0n); - - const claimContract = await manager.loadContract(num.toHex(claimAddress)); - const claimAccount = new Account(manager, claimContract.address, signer, undefined, RPC.ETransactionVersion.V2); - - const balanceSenderBefore = await tokenContract.balance_of(deployer.address); const { transaction_hash } = await factory.cancel(claim); const txFee = BigInt((await manager.getTransactionReceipt(transaction_hash)).actual_fee.amount); // Check balance of the sender is correct - await tokenContract + await token .balance_of(deployer.address) - .should.eventually.equal(balanceSenderBefore + amount + maxFee - txFee); + .should.eventually.equal(balanceSenderBefore + claim.gift_amount + claim.fee_amount - txFee); // Check balance claim address address == 0 - await tokenContract.balance_of(claimAddress).should.eventually.equal(0n); + await token.balance_of(claimAddress).should.eventually.equal(0n); - factory.connect(claimAccount); - await expectRevertWithErrorMessage("gift/already-claimed-or-cancel", () => factory.claim_internal(claim, receiver)); + await expectRevertWithErrorMessage("gift/already-claimed-or-cancel", () => + claimInternal(claim, receiver, claimPrivateKey), + ); }); it(`Test pausable`, async function () { - await manager.restartDevnetAndClearClassCache(); // Deploy factory - const claimAccountClassHash = await manager.declareLocalContract("ClaimAccount"); - const factory = await manager.deployContract("GiftFactory", { - unique: true, - constructorCalldata: [claimAccountClassHash, deployer.address], - }); - const signer = new LegacyStarknetKeyPair(); - const claimPubkey = signer.publicKey; - const amount = 1000000000000000n; - const maxFee = 50000000000000n; - const receiver = "0x42"; + const { factory } = await setupGiftProtocol(); + const receiver = randomReceiver(); + const claimSigner = new LegacyStarknetKeyPair(`0x${encode.buf2hex(ec.starkCurve.utils.randomPrivateKey())}`); - // Make a gift + // approvals const tokenContract = await manager.tokens.feeTokenContract(false); tokenContract.connect(deployer); factory.connect(deployer); - await tokenContract.approve(factory.address, amount + maxFee); + await tokenContract.approve(factory.address, GIFT_AMOUNT + GIFT_MAX_FEE); + // pause / unpause factory.connect(genericAccount); await expectRevertWithErrorMessage("Caller is not the owner", () => factory.pause()); factory.connect(deployer); await factory.pause(); await expectRevertWithErrorMessage("Pausable: paused", () => - factory.deposit(amount, maxFee, tokenContract.address, claimPubkey), + factory.deposit(tokenContract.address, GIFT_AMOUNT, tokenContract.address, GIFT_MAX_FEE, claimSigner.publicKey), ); await factory.unpause(); - await factory.deposit(amount, maxFee, tokenContract.address, claimPubkey); - - // Ensure there is a contract for the claim - const claimAddress = await factory.get_claim_address( - claimAccountClassHash, - deployer.address, - amount, - maxFee, - tokenContract.address, - claimPubkey, - ); - - const claim = { - factory: factory.address, - class_hash: claimAccountClassHash, - sender: deployer.address, - amount: uint256.bnToUint256(amount), - max_fee: maxFee, - token: tokenContract.address, - claim_pubkey: claimPubkey, - }; - - const claimContract = await manager.loadContract(num.toHex(claimAddress)); - const claimAccount = new Account(manager, claimContract.address, signer, undefined, RPC.ETransactionVersion.V2); - - // Check balance of the claim contract is correct - await tokenContract.balance_of(claimAddress).should.eventually.equal(amount + maxFee); - // Check balance receiver address == 0 - await tokenContract.balance_of(receiver).should.eventually.equal(0n); - - factory.connect(claimAccount); - await factory.claim_internal(claim, receiver); + const { claim } = await defaultDepositTestSetup(factory, false, claimSigner.privateKey); + await claimInternal(claim, receiver, claimSigner.privateKey); // Final check + const claimAddress = calculateClaimAddress(claim); const dustBalance = await tokenContract.balance_of(claimAddress); - expect(dustBalance < maxFee).to.be.true; - await tokenContract.balance_of(receiver).should.eventually.equal(amount); + expect(dustBalance < GIFT_MAX_FEE).to.be.true; + await tokenContract.balance_of(receiver).should.eventually.equal(GIFT_AMOUNT); }); });