From 64b5148365d76ecbcaad5f2b3a677a0050aa9f20 Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Tue, 21 Nov 2023 09:01:56 +0100 Subject: [PATCH 1/4] clean up sdk scripts --- app/src/components/Cowllect.tsx | 2 +- sdk/sample.env | 2 +- sdk/src/appData.ts | 629 ++++++++++++++++++++------------ sdk/src/createOrderData.ts | 36 +- sdk/src/eoaThroughSafe.ts | 52 ++- sdk/src/main.ts | 73 ++-- sdk/src/types.ts | 10 - 7 files changed, 493 insertions(+), 311 deletions(-) delete mode 100644 sdk/src/types.ts diff --git a/app/src/components/Cowllect.tsx b/app/src/components/Cowllect.tsx index b1e4966..1b53e01 100644 --- a/app/src/components/Cowllect.tsx +++ b/app/src/components/Cowllect.tsx @@ -52,7 +52,7 @@ const Cowllect = ({ sdk, safeInfo, offChainSigningEnabled }: OwnProps): React.Re async function postAppData(appData: AppData): Promise { let { hash, data } = appData; - const url = `https://api.cow.fi/xdai/api/v1/app_data/${hash}`; + const url = `${COW_API}/app_data/${hash}`; const requestBody = { fullAppData: data, }; diff --git a/sdk/sample.env b/sdk/sample.env index e0cbc40..b4790af 100644 --- a/sdk/sample.env +++ b/sdk/sample.env @@ -1,4 +1,4 @@ # This is for eoaClaimAndSwap export PRIVATE_KEY= export NODE_URL=https://rpc.gnosischain.com/ -export COW_API=https://barn.api.cow.fi/xdai/api/v1 +export COW_API=https://api.cow.fi/xdai/api/v1 diff --git a/sdk/src/appData.ts b/sdk/src/appData.ts index c2b684e..3eab863 100644 --- a/sdk/src/appData.ts +++ b/sdk/src/appData.ts @@ -1,253 +1,408 @@ import { BigNumber, ethers } from "ethers"; -import axios from "axios"; -import { AppData, CowHook } from "./types"; - -function PermittableToken( - provider: ethers.providers.JsonRpcProvider, - address: string -): ethers.Contract { - return new ethers.Contract( - address, - [ - ` - function decimals() view returns (uint8) - `, - ` - function name() view returns (string) - `, - ` - function version() view returns (string) - `, - ` - function nonces(address owner) view returns (uint256) - `, - ` - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) - `, - ], - provider - ); -} -/// Permit Token Hook! -export async function buildPermitHook( - wallet: ethers.Wallet, - provider: ethers.providers.JsonRpcProvider, - spender: string, - amount: BigNumber, - permittableTokenAddress: string, - chainId: number -): Promise { - const token = PermittableToken(provider, permittableTokenAddress); - const permit = { - owner: wallet.address, - spender, - value: amount, - nonce: await token.nonces(wallet.address), - deadline: ethers.constants.MaxUint256, - }; - const permitSignature = ethers.utils.splitSignature( - await wallet._signTypedData( - { - name: await token.name(), - version: await token.version(), - chainId, - verifyingContract: token.address, - }, - { - Permit: [ - { name: "owner", type: "address" }, - { name: "spender", type: "address" }, - { name: "value", type: "uint256" }, - { name: "nonce", type: "uint256" }, - { name: "deadline", type: "uint256" }, - ], - }, - permit - ) - ); - const permitParams = [ - permit.owner, - permit.spender, - permit.value, - permit.deadline, - permitSignature.v, - permitSignature.r, - permitSignature.s, - ]; - const permitHook = { - target: token.address, - callData: token.interface.encodeFunctionData("permit", permitParams), - gasLimit: `${await token.estimateGas.permit(...permitParams)}`, - }; - console.log("permit hook:", permitHook); - return permitHook; -} +export type AppData = { + hash: string; + data: string; +}; -/// Hook to Claim Validator Rewards for withdrawalAddress -export function buildClaimHook( - provider: ethers.providers.JsonRpcProvider, - withdrawalAddress: string, - claimContract: string -): CowHook { - const CLAIM_CONTRACT = new ethers.Contract( - claimContract, - [ - `function withdrawableAmount(address user) view returns (u256)`, - `function claimWithdrawal(address _address) public`, - ], - provider - ); - - const claimHook = { - target: CLAIM_CONTRACT.address, - callData: CLAIM_CONTRACT.interface.encodeFunctionData("claimWithdrawal", [ - withdrawalAddress, - ]), - // Example Tx: https://gnosisscan.io/tx/0x9501e8cbe873126bfb52ceab26a8644f1f8607f0de2937fa29d2112569480c59 - // Gas Limit: 82264 - // gasLimit: `${await DEPOSIT_CONTRACT.estimateGas.claimWithdrawal(...withdrawalAddress)}`, - // Now it doesn't need to be async! - gasLimit: "82264", - }; - return claimHook; -} +export type CowHook = { + target: string; + callData: string; + gasLimit: string; +}; -export function generateAppData( - preHooks: object[], - postHooks: object[] -): AppData { - const appData = JSON.stringify({ - appCode: "CoW Swap", - version: "0.9.0", - metadata: { - hooks: { - pre: preHooks, - post: postHooks, - }, - }, - }); - - const appHash = ethers.utils.id(appData); - console.log(`Constructed AppData with Hash ${appHash}`); - - console.log("App Data Content:", appData); - // https://api.cow.fi/docs/#/ - // This needs to be posted to https://api.cow.fi/xdai/api/v1/app_data/{app_hash} - // with payload {"fullAppData": "{appData}"} - // CURL: Note that this has to be an escaped JSON string. - // curl -X 'PUT' \ - // 'https://api.cow.fi/xdai/api/v1/app_data/{APP_HASH}' \ - // -H 'accept: application/json' \ - // -H 'Content-Type: application/json' \ - // -d '{ - // "fullAppData":"{\"appCode\":\"CoW Swap\",\"metadata\":{\"hooks\":{\"post\":[],\"pre\":[{\"callData\":\"0xa3066aab000000000000000000000000{WITHDRAWAL_ADDRESS}\",\"gasLimit\":\"82264\",\"target\":\"0x0B98057eA310F4d31F2a452B414647007d1645d9\"}],\"version\":\"0.1.0\"}},\"version\":\"0.10.0\"}"}' - return { hash: appHash, data: appData }; -} +export type PermitHookParams = { + // EOA wallet must be used to sign permit. + // TODO - support Multisig wallet. + wallet: ethers.Wallet; + tokenAddress: string; + spender: string; + amount: BigNumber; +}; -export async function postAppData(appData: AppData): Promise { - const provider = new ethers.providers.JsonRpcProvider( - process.env.NODE_URL || "https://rpc.gnosischain.com/" - ); +export class HookBuilder { + readonly provider: ethers.providers.JsonRpcProvider; + readonly cowApi: string; - // TODO - EOA permit SAFE via set allowance and encode transfer from hook. - let { hash, data } = appData; + /** + * + * @param web3 - rpc connection to web3 environment. + * @param cowApiUrl - CoWSwap API endpoint. + */ + constructor(web3: ethers.providers.JsonRpcProvider, cowApiUrl: string) { + this.provider = web3; + this.cowApi = cowApiUrl; + } - const url = `https://api.cow.fi/xdai/api/v1/app_data/${hash}`; - const requestBody = { - fullAppData: data, - }; + /// Builds ERC20 Permit hool for an externally owned account. + async permitHook(params: PermitHookParams) { + const { wallet, tokenAddress, spender, amount } = params; + const token = new ethers.Contract( + tokenAddress, + [ + `function decimals() view returns (uint8)`, + `function name() view returns (string)`, + `function version() view returns (string)`, + `function nonces(address owner) view returns (uint256)`, + ` + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) + `, + ], + this.provider + ); + const [name, version, network, nonce] = await Promise.all([ + token.name(), + token.version(), + this.provider.getNetwork(), + token.nonces(wallet.address), + ]); + const permit = { + owner: wallet.address, + spender, + value: amount, + nonce, + deadline: ethers.constants.MaxUint256, + }; + const permitSignature = ethers.utils.splitSignature( + await wallet._signTypedData( + { + // TODO - parallel requests! + name, + version, + chainId: network.chainId, + verifyingContract: token.address, + }, + { + Permit: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + { name: "value", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], + }, + permit + ) + ); + const permitParams = [ + permit.owner, + permit.spender, + permit.value, + permit.deadline, + permitSignature.v, + permitSignature.r, + permitSignature.s, + ]; + const permitHook = { + target: token.address, + callData: token.interface.encodeFunctionData("permit", permitParams), + gasLimit: `${await token.estimateGas.permit(...permitParams)}`, + }; + return permitHook; + } - try { - await axios.put(url, requestBody, { - headers: { - Accept: "application/json", - "Content-Type": "application/json", + /** Builds a claimHook for any permissionless claim function + * with signature `function ${claimFunctionName}(address)` + * @param claimantAddress address to be passed to claim Function (i.e. claim for) + * @param claimContractAddress contract where claim should be called. + * @param claimFunctionName [optional] name of claim function (defaults to `claimWithdrawal`) + * @returns CoWHook for claim + */ + async permissionlessClaimHook( + claimantAddress: string, + claimContractAddress: string, + claimFunctionName?: string + ): Promise { + let claimName = claimFunctionName ? claimFunctionName : "claimWithdrawal"; + let contract = new ethers.Contract( + claimContractAddress, + [`function ${claimName}(address)`], + // provider required to estimate gas. + this.provider + ); + const callData = contract.interface.encodeFunctionData(claimName, [ + claimantAddress, + ]); + return { + target: contract.address, + callData, + gasLimit: `${await contract.estimateGas.claimWithdrawal( + claimantAddress + )}`, + }; + } + + /** Generates call data and gas limit of a call to `transferBalanceFrom` call. + * @param tokenAddress address of token to be transfered + * @param from address of account whose balance is being transfered + * @param to address of account who to receive the full balance of `tokenAddress` from `from` + * @returns CoWHook representing this interaction. + */ + async transferBalanceFromHook( + tokenAddress: string, + from: string, + to: string + ): Promise { + // TODO - configure by network (or deploy to same address) + const TRANSFER_BALANCE_FROM_CONTRACT = + "0xD4121d2d90CE7C5F7FB66c4E96815fc377481635"; + const transfer = new ethers.Contract( + TRANSFER_BALANCE_FROM_CONTRACT, + [`function transferBalanceFrom(address token, address from, address to)`], + this.provider + ); + const params = [tokenAddress, from, to]; + const callData = transfer.interface.encodeFunctionData( + "transferBalanceFrom", + params + ); + console.log("encoded data for transferBalanceFromHook"); + return { + target: transfer.address, + callData, + gasLimit: `${await transfer.estimateGas.transferBalanceFrom(...params)}`, + }; + } + + /** + * Constructs valid (stringified) AppData JSON with the given hooks + * @param preHooks pre-settlement Interactions + * @param postHooks post-settlement Interactions + * @returns AppData structure (with content and hash) + */ + generateAppData(preHooks: CowHook[], postHooks: CowHook[]): AppData { + const appData = JSON.stringify({ + // TODO - use app_data SDK for other fields (this is only for hooks) + appCode: "Karvest Finance", + version: "0.9.0", + metadata: { + hooks: { + pre: preHooks, + post: postHooks, + }, }, }); - console.log("App data posted successfully"); - } catch (error: any) { - console.error("Error updating app data:", error.message); + const appHash = ethers.utils.id(appData); + console.log(`Constructed AppData with Hash ${appHash}`); + // https://api.cow.fi/docs/#/ + return { hash: appHash, data: appData }; } -} -export async function buildTransferBalanceFromHook( - provider: ethers.providers.JsonRpcProvider, - tokenAddress: string, - from: string, - to: string -): Promise { - const TRANSFER_BALANCE_FROM_CONTRACT = - "0xD4121d2d90CE7C5F7FB66c4E96815fc377481635"; - console.log("tokenAddres:", tokenAddress); - console.log("from:", from); - console.log("to:", from); - const transfer = new ethers.Contract( - TRANSFER_BALANCE_FROM_CONTRACT, - [ - `function transferBalanceFrom(address token, address from, address to) public`, - ], - provider - ); - console.log("transfer:", transfer); - const params = [tokenAddress, from, to]; - return { - target: transfer.address, - callData: transfer.interface.encodeFunctionData( - "transferBalanceFrom", - params - ), - gasLimit: `${await transfer.estimateGas.transferBalanceFrom(...params)}`, - }; -} + async postAppData({ hash, data }: AppData): Promise { + const url = `${this.cowApi}/app_data/${hash}`; + const requestBody = { fullAppData: data }; -export async function mixedEoaSafeAppData( - eoaAddress: string, - safeAddress: string, - claimContractAddress: string, - claimTokenAddress: string -): Promise { - const provider = new ethers.providers.JsonRpcProvider( - process.env.NODE_URL || "https://rpc.gnosischain.com/" - ); - // TODO - EOA permit SAFE via set allowance and encode transfer from hook. - let appData = await generateAppData( - [ - buildClaimHook(provider, safeAddress, claimContractAddress), - await buildTransferBalanceFromHook( - provider, - claimTokenAddress, - eoaAddress, - safeAddress - ), - ], - [] - ); - await postAppData(appData); - return appData; -} + try { + const response = await fetch(url, { + method: "PUT", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }); -export async function safeOnlyAppData( - safeAddress: string, - claimContractAddress: string -): Promise { - const provider = new ethers.providers.JsonRpcProvider( - process.env.NODE_URL || "https://rpc.gnosischain.com/" - ); - // TODO - EOA permit SAFE via set allowance and encode transfer from hook. - let appData = generateAppData( - [buildClaimHook(provider, safeAddress, claimContractAddress)], - [] - ); - await postAppData(appData); - return appData; + if (response.ok) { + console.log(`App data with hash ${hash} posted successfully`); + } else { + throw new Error( + `Error updating app data: ${response.status} - ${response.statusText}` + ); + } + } catch (error) { + console.error(`Error posting app data ${error}`); + } + } } + +// function PermittableToken( +// provider: ethers.providers.JsonRpcProvider, +// address: string +// ): ethers.Contract { +// return new ethers.Contract( +// address, +// [ +// `function decimals() view returns (uint8)`, +// `function name() view returns (string)`, +// `function version() view returns (string)`, +// `function nonces(address owner) view returns (uint256)`, +// ` +// function permit( +// address owner, +// address spender, +// uint256 value, +// uint256 deadline, +// uint8 v, +// bytes32 r, +// bytes32 s +// ) +// `, +// ], +// provider +// ); +// } + +// /// Permit Token Hook! +// export async function buildPermitHook( +// wallet: ethers.Wallet, +// provider: ethers.providers.JsonRpcProvider, +// spender: string, +// amount: BigNumber, +// permittableTokenAddress: string, +// chainId: number +// ): Promise { +// const token = PermittableToken(provider, permittableTokenAddress); +// const permit = { +// owner: wallet.address, +// spender, +// value: amount, +// nonce: await token.nonces(wallet.address), +// deadline: ethers.constants.MaxUint256, +// }; +// const permitSignature = ethers.utils.splitSignature( +// await wallet._signTypedData( +// { +// name: await token.name(), +// version: await token.version(), +// chainId, +// verifyingContract: token.address, +// }, +// { +// Permit: [ +// { name: "owner", type: "address" }, +// { name: "spender", type: "address" }, +// { name: "value", type: "uint256" }, +// { name: "nonce", type: "uint256" }, +// { name: "deadline", type: "uint256" }, +// ], +// }, +// permit +// ) +// ); +// const permitParams = [ +// permit.owner, +// permit.spender, +// permit.value, +// permit.deadline, +// permitSignature.v, +// permitSignature.r, +// permitSignature.s, +// ]; +// const permitHook = { +// target: token.address, +// callData: token.interface.encodeFunctionData("permit", permitParams), +// gasLimit: `${await token.estimateGas.permit(...permitParams)}`, +// }; +// console.log("permit hook:", permitHook); +// return permitHook; +// } + +// /// Hook to Claim Validator Rewards for withdrawalAddress +// export function buildClaimHook( +// provider: ethers.providers.JsonRpcProvider, +// withdrawalAddress: string, +// claimContract: string +// ): CowHook { +// const CLAIM_CONTRACT = new ethers.Contract( +// claimContract, +// [ +// `function withdrawableAmount(address user) view returns (u256)`, +// `function claimWithdrawal(address _address) public`, +// ], +// provider +// ); + +// const claimHook = { +// target: CLAIM_CONTRACT.address, +// callData: CLAIM_CONTRACT.interface.encodeFunctionData("claimWithdrawal", [ +// withdrawalAddress, +// ]), +// // Example Tx: https://gnosisscan.io/tx/0x9501e8cbe873126bfb52ceab26a8644f1f8607f0de2937fa29d2112569480c59 +// // Gas Limit: 82264 +// // gasLimit: `${await DEPOSIT_CONTRACT.estimateGas.claimWithdrawal(...withdrawalAddress)}`, +// // Now it doesn't need to be async! +// gasLimit: "82264", +// }; +// return claimHook; +// } + +// export function generateAppData( +// preHooks: object[], +// postHooks: object[] +// ): AppData { +// const appData = JSON.stringify({ +// appCode: "CoW Swap", +// version: "0.9.0", +// metadata: { +// hooks: { +// pre: preHooks, +// post: postHooks, +// }, +// }, +// }); + +// const appHash = ethers.utils.id(appData); +// console.log(`Constructed AppData with Hash ${appHash}`); + +// console.log("App Data Content:", appData); +// // https://api.cow.fi/docs/#/ +// return { hash: appHash, data: appData }; +// } + +// export async function postAppData(appData: AppData): Promise { +// // TODO - EOA permit SAFE via set allowance and encode transfer from hook. +// let { hash, data } = appData; + +// const url = `https://api.cow.fi/xdai/api/v1/app_data/${hash}`; +// const requestBody = { +// fullAppData: data, +// }; + +// try { +// await axios.put(url, requestBody, { +// headers: { +// Accept: "application/json", +// "Content-Type": "application/json", +// }, +// }); + +// console.log("App data posted successfully"); +// } catch (error: any) { +// console.error("Error updating app data:", error.message); +// } +// } + +// export async function buildTransferBalanceFromHook( +// provider: ethers.providers.JsonRpcProvider, +// tokenAddress: string, +// from: string, +// to: string +// ): Promise { +// const TRANSFER_BALANCE_FROM_CONTRACT = +// "0xD4121d2d90CE7C5F7FB66c4E96815fc377481635"; +// const transfer = new ethers.Contract( +// TRANSFER_BALANCE_FROM_CONTRACT, +// [ +// `function transferBalanceFrom(address token, address from, address to) public`, +// ], +// provider +// ); +// const params = [tokenAddress, from, to]; +// return { +// target: transfer.address, +// callData: transfer.interface.encodeFunctionData( +// "transferBalanceFrom", +// params +// ), +// gasLimit: `${await transfer.estimateGas.transferBalanceFrom(...params)}`, +// }; +// } diff --git a/sdk/src/createOrderData.ts b/sdk/src/createOrderData.ts index 24b8815..5b3606d 100644 --- a/sdk/src/createOrderData.ts +++ b/sdk/src/createOrderData.ts @@ -1,5 +1,7 @@ import { ethers } from "ethers"; -import { safeOnlyAppData } from "./appData"; +import { AppData, HookBuilder } from "./appData"; +import { generateOrderSalt } from "./utils"; +import { CLAIM_AND_SWAP_CONTRACT, COW_API } from "./constants"; /// WXDAI const CLAIM_TOKEN_ADDRESS = "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d"; @@ -11,24 +13,38 @@ const CLAIM_CONTRACT_ADDRESS = "0xf07afcee9dd0b859edd41603a3d725b70086fef6"; const SAFE_ADDRESS = "0x608Acd7d1c01439b351FEfAFf7636A136aF3Da81"; -const CLAIM_AND_SWAP_CONTRACT = "0x35f29f3cb53bddb11b6e286a0454a9224dd3adaa"; +async function safeOnlyAppData( + provider: ethers.providers.JsonRpcProvider, + safeAddress: string, + claimContractAddress: string +): Promise { + let builder = new HookBuilder(provider, COW_API); + let appData = builder.generateAppData( + [await builder.permissionlessClaimHook(safeAddress, claimContractAddress)], + [] + ); + await builder.postAppData(appData); + return appData; +} -/// async function buildCreateOrderData() { - // TODO - generate and post app data use hash below. - const appData = await safeOnlyAppData(SAFE_ADDRESS, CLAIM_CONTRACT_ADDRESS); + const provider = new ethers.providers.JsonRpcProvider( + process.env.NODE_URL || "https://rpc.gnosischain.com/" + ); + const { hash: appDataHash } = await safeOnlyAppData( + provider, + SAFE_ADDRESS, + CLAIM_CONTRACT_ADDRESS + ); const staticOrderData = ethers.utils.defaultAbiCoder.encode( ["address", "address", "address", "bytes32"], - [CLAIM_TOKEN_ADDRESS, BUY_TOKEN_ADDRESS, SAFE_ADDRESS, appData.hash] + [CLAIM_TOKEN_ADDRESS, BUY_TOKEN_ADDRESS, SAFE_ADDRESS, appDataHash] ); const params = [ // handlerAddress CLAIM_AND_SWAP_CONTRACT, - // Order Salt - // keccak("karvest"); - // TODO - This needs to be changed for every new order placement. - "0x5cac3505cb5ef10c425e9385b61b0bc2f433203871d18aca2409326bd98b0529", + generateOrderSalt(), staticOrderData, ]; diff --git a/sdk/src/eoaThroughSafe.ts b/sdk/src/eoaThroughSafe.ts index 7705921..bc7c5ca 100644 --- a/sdk/src/eoaThroughSafe.ts +++ b/sdk/src/eoaThroughSafe.ts @@ -1,43 +1,59 @@ import { ethers } from "ethers"; -import { mixedEoaSafeAppData } from "./appData"; +import { generateOrderSalt } from "./utils"; +import { AppData, HookBuilder } from "./appData"; +import { + COW_API, + GNO_CLAIM_CONTRACT_ADDRESS, + CLAIM_AND_SWAP_CONTRACT, + GNO_TOKEN_ADDRESS, +} from "./constants"; -/// GNO -const CLAIM_TOKEN_ADDRESS = "0x9c58bacc331c9aa871afd802db6379a98e80cedb"; /// COW on Gnosis Chain const BUY_TOKEN_ADDRESS = "0x177127622c4A00F3d409B75571e12cB3c8973d3c"; - -/// OUR MOCK Deposit Contract -const CLAIM_CONTRACT_ADDRESS = "0x9c58bacc331c9aa871afd802db6379a98e80cedb"; - const SAFE_ADDRESS = "0x608Acd7d1c01439b351FEfAFf7636A136aF3Da81"; -const CLAIM_AND_SWAP_CONTRACT = "0x35f29f3cb53bddb11b6e286a0454a9224dd3adaa"; +async function generateMixedEoaSafeAppData( + provider: ethers.providers.JsonRpcProvider, + eoaAddress: string, + safeAddress: string, + claimContractAddress: string, + claimTokenAddress: string +): Promise { + let builder = new HookBuilder(provider, COW_API); + // TODO - EOA permit SAFE via set allowance and encode transfer from hook. + let preHooks = await Promise.all([ + builder.permissionlessClaimHook(safeAddress, claimContractAddress), + builder.transferBalanceFromHook(claimTokenAddress, eoaAddress, safeAddress), + ]); + let appData = await builder.generateAppData(preHooks, []); + await builder.postAppData(appData); + return appData; +} -/// async function buildCreateOrderData() { const eoaAddress = process.env.ETH1_WITHDRAW_ADDRESS; if (eoaAddress === undefined) throw Error("Invalid env var ETH1_WITHDRAW_ADDRESS"); - const appData = await mixedEoaSafeAppData( + const provider = new ethers.providers.JsonRpcProvider( + process.env.NODE_URL || "https://rpc.gnosischain.com/" + ); + const appData = await generateMixedEoaSafeAppData( + provider, eoaAddress, SAFE_ADDRESS, - CLAIM_CONTRACT_ADDRESS, - CLAIM_TOKEN_ADDRESS + GNO_CLAIM_CONTRACT_ADDRESS, + GNO_TOKEN_ADDRESS ); const staticOrderData = ethers.utils.defaultAbiCoder.encode( ["address", "address", "address", "bytes32"], - [CLAIM_TOKEN_ADDRESS, BUY_TOKEN_ADDRESS, SAFE_ADDRESS, appData.hash] + [GNO_CLAIM_CONTRACT_ADDRESS, BUY_TOKEN_ADDRESS, SAFE_ADDRESS, appData.hash] ); const params = [ // handlerAddress CLAIM_AND_SWAP_CONTRACT, // Order Salt - // keccak("karvest"); - // TODO - This needs to be changed for every new order placement. - // ethers.utils.id() - "0x5cac3505cb5ef10c425e9385b61b0bc2f433203871d18aca2409326bd98b0529", - // ethers.utils.id(Date.now().toString()), + generateOrderSalt(), staticOrderData, ]; diff --git a/sdk/src/main.ts b/sdk/src/main.ts index 6b551ad..5364ab2 100644 --- a/sdk/src/main.ts +++ b/sdk/src/main.ts @@ -1,5 +1,13 @@ import { ethers } from "ethers"; -import { generateAppData, buildClaimHook, buildPermitHook } from "./appData"; +import { HookBuilder } from "./appData"; +import { + SETTLEMENT_CONTRACT_ADDRESS, + COW_API, + WEB3_PROVIDER, + GNO_TOKEN_ADDRESS, + BUY_TOKEN_ADDRESS, + GNO_CLAIM_CONTRACT_ADDRESS, +} from "./constants"; async function eoaClaimAndSwap( wallet: ethers.Wallet, @@ -9,26 +17,38 @@ async function eoaClaimAndSwap( claimAddress: string ) { const { chainId } = await provider.getNetwork(); + let builder = new HookBuilder(provider, COW_API); console.log(`connected to chain ${chainId} with account ${wallet.address}`); - const appData = generateAppData( - [ - buildClaimHook(provider, wallet.address, claimAddress), - await buildPermitHook( - wallet, - provider, - SETTLEMENT_CONTRACT_ADDRESS, - ethers.constants.MaxUint256, - sellToken, - chainId - ), - ], - [] + console.log("Building claim and permit hook"); + let preHooks = await Promise.all([ + builder.permissionlessClaimHook(wallet.address, claimAddress), + builder.permitHook({ + wallet, + tokenAddress: sellToken, + amount: ethers.constants.MaxUint256, + spender: SETTLEMENT_CONTRACT_ADDRESS, + }), + ]); + + const appData = builder.generateAppData(preHooks, []); + let contract = new ethers.Contract( + claimAddress, + [`function withdrawableAmount(address)`], + provider ); + const claimAmount = await contract + .connect(wallet) + .withdrawableAmount(wallet.address); + if (!(claimAmount > 0)) { + console.log("Nothing to claim"); + return; + } + console.log("Claim Amount:", claimAmount); const orderConfig = { sellToken, buyToken, receiver: ethers.constants.AddressZero, - sellAmount: `${ethers.utils.parseUnits("0.1", 18)}`, + sellAmount: claimAmount, kind: "sell", partiallyFillable: false, sellTokenBalance: "erc20", @@ -106,30 +126,15 @@ async function eoaClaimAndSwap( console.log("order:", orderUid); } -/// GNO Token -const CLAIM_TOKEN_ADDRESS = "0x9c58bacc331c9aa871afd802db6379a98e80cedb"; -/// COW on Gnosis Chain -const BUY_TOKEN_ADDRESS = "0x177127622c4A00F3d409B75571e12cB3c8973d3c"; -/// SBD Deposit Contract -const CLAIM_CONTRACT_ADDRESS = "0x0B98057eA310F4d31F2a452B414647007d1645d9"; - -const SETTLEMENT_CONTRACT_ADDRESS = - "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"; - -const provider = new ethers.providers.JsonRpcProvider( - process.env.NODE_URL || "https://rpc.gnosischain.com/" -); -const COW_API = "https://barn.api.cow.fi/xdai/api/v1"; - const wallet = new ethers.Wallet( process.env.PRIVATE_KEY || "0xBADPRIVATEKEY", - provider + WEB3_PROVIDER ); eoaClaimAndSwap( wallet, - provider, - CLAIM_TOKEN_ADDRESS, + WEB3_PROVIDER, + GNO_TOKEN_ADDRESS, BUY_TOKEN_ADDRESS, - CLAIM_CONTRACT_ADDRESS + GNO_CLAIM_CONTRACT_ADDRESS ); diff --git a/sdk/src/types.ts b/sdk/src/types.ts deleted file mode 100644 index 54f8452..0000000 --- a/sdk/src/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type AppData = { - hash: string; - data: string; -}; - -export type CowHook = { - target: string; - callData: string; - gasLimit: string; -}; From dc249f02fa93d488d2e77c4e3eeea2a84efe7bee Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Tue, 21 Nov 2023 09:02:08 +0100 Subject: [PATCH 2/4] add new files --- sdk/src/constants.ts | 28 ++++++++++++++++++++++++++++ sdk/src/utils.ts | 7 +++++++ 2 files changed, 35 insertions(+) create mode 100644 sdk/src/constants.ts create mode 100644 sdk/src/utils.ts diff --git a/sdk/src/constants.ts b/sdk/src/constants.ts new file mode 100644 index 0000000..08306a2 --- /dev/null +++ b/sdk/src/constants.ts @@ -0,0 +1,28 @@ +import { ethers } from "ethers"; + +/// GNO Token +const GNO_TOKEN_ADDRESS = "0x9c58bacc331c9aa871afd802db6379a98e80cedb"; +/// COW on Gnosis Chain +const BUY_TOKEN_ADDRESS = "0x177127622c4A00F3d409B75571e12cB3c8973d3c"; +/// SBD Deposit Contract +const GNO_CLAIM_CONTRACT_ADDRESS = "0x0B98057eA310F4d31F2a452B414647007d1645d9"; +/// Programatic Order Contract +const CLAIM_AND_SWAP_CONTRACT = "0x35f29f3cb53bddb11b6e286a0454a9224dd3adaa"; + +const SETTLEMENT_CONTRACT_ADDRESS = + "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"; + +const WEB3_PROVIDER = new ethers.providers.JsonRpcProvider( + process.env.NODE_URL || "https://rpc.gnosischain.com/" +); +const COW_API = process.env.COW_API || "https://api.cow.fi/xdai/api/v1"; + +export { + CLAIM_AND_SWAP_CONTRACT, + GNO_TOKEN_ADDRESS, + BUY_TOKEN_ADDRESS, + GNO_CLAIM_CONTRACT_ADDRESS, + SETTLEMENT_CONTRACT_ADDRESS, + WEB3_PROVIDER, + COW_API, +}; diff --git a/sdk/src/utils.ts b/sdk/src/utils.ts new file mode 100644 index 0000000..6e538a9 --- /dev/null +++ b/sdk/src/utils.ts @@ -0,0 +1,7 @@ +import { ethers } from "ethers"; + +export function generateOrderSalt(): string { + return ethers.utils.id( + ethers.utils.keccak256("karvest" + Date.now().toString()) + ); +} From 99371e57adbb4d1d3fd4f2315d05367408ad990e Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Tue, 21 Nov 2023 09:09:42 +0100 Subject: [PATCH 3/4] move type to own file --- sdk/src/appData.ts | 21 +-------------------- sdk/src/createOrderData.ts | 3 ++- sdk/src/eoaThroughSafe.ts | 3 ++- sdk/src/main.ts | 29 ++++++++++++++++++++++------- sdk/src/types.ts | 21 +++++++++++++++++++++ 5 files changed, 48 insertions(+), 29 deletions(-) create mode 100644 sdk/src/types.ts diff --git a/sdk/src/appData.ts b/sdk/src/appData.ts index 3eab863..2d81c99 100644 --- a/sdk/src/appData.ts +++ b/sdk/src/appData.ts @@ -1,24 +1,5 @@ import { BigNumber, ethers } from "ethers"; - -export type AppData = { - hash: string; - data: string; -}; - -export type CowHook = { - target: string; - callData: string; - gasLimit: string; -}; - -export type PermitHookParams = { - // EOA wallet must be used to sign permit. - // TODO - support Multisig wallet. - wallet: ethers.Wallet; - tokenAddress: string; - spender: string; - amount: BigNumber; -}; +import { AppData, CowHook, PermitHookParams } from "./types"; export class HookBuilder { readonly provider: ethers.providers.JsonRpcProvider; diff --git a/sdk/src/createOrderData.ts b/sdk/src/createOrderData.ts index 5b3606d..545177b 100644 --- a/sdk/src/createOrderData.ts +++ b/sdk/src/createOrderData.ts @@ -1,7 +1,8 @@ import { ethers } from "ethers"; -import { AppData, HookBuilder } from "./appData"; +import { HookBuilder } from "./appData"; import { generateOrderSalt } from "./utils"; import { CLAIM_AND_SWAP_CONTRACT, COW_API } from "./constants"; +import { AppData } from "./types"; /// WXDAI const CLAIM_TOKEN_ADDRESS = "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d"; diff --git a/sdk/src/eoaThroughSafe.ts b/sdk/src/eoaThroughSafe.ts index bc7c5ca..64a7d8d 100644 --- a/sdk/src/eoaThroughSafe.ts +++ b/sdk/src/eoaThroughSafe.ts @@ -1,12 +1,13 @@ import { ethers } from "ethers"; import { generateOrderSalt } from "./utils"; -import { AppData, HookBuilder } from "./appData"; +import { HookBuilder } from "./appData"; import { COW_API, GNO_CLAIM_CONTRACT_ADDRESS, CLAIM_AND_SWAP_CONTRACT, GNO_TOKEN_ADDRESS, } from "./constants"; +import { AppData } from "./types"; /// COW on Gnosis Chain const BUY_TOKEN_ADDRESS = "0x177127622c4A00F3d409B75571e12cB3c8973d3c"; diff --git a/sdk/src/main.ts b/sdk/src/main.ts index 5364ab2..9bb12eb 100644 --- a/sdk/src/main.ts +++ b/sdk/src/main.ts @@ -8,17 +8,14 @@ import { BUY_TOKEN_ADDRESS, GNO_CLAIM_CONTRACT_ADDRESS, } from "./constants"; +import { AppData } from "./types"; -async function eoaClaimAndSwap( - wallet: ethers.Wallet, +async function eoaClaimAndPermitAppData( provider: ethers.providers.JsonRpcProvider, sellToken: string, - buyToken: string, claimAddress: string -) { - const { chainId } = await provider.getNetwork(); +): Promise { let builder = new HookBuilder(provider, COW_API); - console.log(`connected to chain ${chainId} with account ${wallet.address}`); console.log("Building claim and permit hook"); let preHooks = await Promise.all([ builder.permissionlessClaimHook(wallet.address, claimAddress), @@ -30,7 +27,23 @@ async function eoaClaimAndSwap( }), ]); - const appData = builder.generateAppData(preHooks, []); + return builder.generateAppData(preHooks, []); +} + +async function eoaClaimAndSwap( + wallet: ethers.Wallet, + provider: ethers.providers.JsonRpcProvider, + sellToken: string, + buyToken: string, + claimAddress: string +) { + const { chainId } = await provider.getNetwork(); + console.log(`connected to chain ${chainId} with account ${wallet.address}`); + const appData = await eoaClaimAndPermitAppData( + provider, + sellToken, + claimAddress + ); let contract = new ethers.Contract( claimAddress, [`function withdrawableAmount(address)`], @@ -43,6 +56,7 @@ async function eoaClaimAndSwap( console.log("Nothing to claim"); return; } + console.log("Claim Amount:", claimAmount); const orderConfig = { sellToken, @@ -76,6 +90,7 @@ async function eoaClaimAndSwap( const orderData = { ...orderConfig, sellAmount: quote.sellAmount, + // 1% slippage tolerance. buyAmount: `${ethers.BigNumber.from(quote.buyAmount).mul(99).div(100)}`, validTo: quote.validTo, appData: appData.hash, diff --git a/sdk/src/types.ts b/sdk/src/types.ts new file mode 100644 index 0000000..42ab870 --- /dev/null +++ b/sdk/src/types.ts @@ -0,0 +1,21 @@ +import { BigNumber, ethers } from "ethers"; + +export type AppData = { + hash: string; + data: string; +}; + +export type CowHook = { + target: string; + callData: string; + gasLimit: string; +}; + +export type PermitHookParams = { + // EOA wallet must be used to sign permit. + // TODO - support Multisig wallet. + wallet: ethers.Wallet; + tokenAddress: string; + spender: string; + amount: BigNumber; +}; From 30774a3e3684e33cc7ce48e8ec08deecd7792980 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 21 Nov 2023 09:17:04 +0100 Subject: [PATCH 4/4] Update app/src/components/Cowllect.tsx --- app/src/components/Cowllect.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/Cowllect.tsx b/app/src/components/Cowllect.tsx index 1b53e01..b1e4966 100644 --- a/app/src/components/Cowllect.tsx +++ b/app/src/components/Cowllect.tsx @@ -52,7 +52,7 @@ const Cowllect = ({ sdk, safeInfo, offChainSigningEnabled }: OwnProps): React.Re async function postAppData(appData: AppData): Promise { let { hash, data } = appData; - const url = `${COW_API}/app_data/${hash}`; + const url = `https://api.cow.fi/xdai/api/v1/app_data/${hash}`; const requestBody = { fullAppData: data, };