first implementation of token minting and approval

This commit is contained in:
stubbsta 2025-05-23 14:37:26 +02:00
parent cba55f6fb5
commit 2aa0a64349

View File

@ -81,14 +81,157 @@ proc getForgePath*(): string =
forgePath = joinPath(forgePath, ".foundry/bin/forge")
return $forgePath
proc deployTestToken*(): Future[string] {.async.} =
contract(ERC20Token):
proc approve(spender: Address, amount: UInt256)
proc allowance(owner: Address, spender: Address): UInt256 {.view.}
proc balanceOf(account: Address): UInt256 {.view.}
proc mint(recipient: Address, amount: UInt256)
proc getTokenBalance*(
web3: Web3, tokenAddress: Address, account: Address
): Future[UInt256] {.async.} =
let token = web3.contractSender(ERC20Token, tokenAddress)
return await token.balanceOf(account).call()
proc ethToWei(eth: UInt256): UInt256 =
eth * 1000000000000000000.u256
proc sendMintCall*(
web3: Web3,
accountFrom: Address,
tokenAddress: Address,
recipientAddress: Address,
amountTokens: UInt256,
recipientBalanceBeforeExpectedTokens: Option[UInt256] = none(UInt256),
): Future[TxHash] {.async.} =
# Create mint transaction
let mintSelector = "0x40c10f19" # Pad the address and amount to 32 bytes each
let addressHex = recipientAddress.toHex()
let paddedAddress = addressHex.align(64, '0')
let amountHex = amountTokens.toHex()
let amountWithout0x =
if amountHex.startsWith("0x"):
amountHex[2 .. ^1]
else:
amountHex
let paddedAmount = amountWithout0x.align(64, '0') # Create the call data
let mintCallData = mintSelector & paddedAddress & paddedAmount # Get gas price
let gasPrice = int(await web3.provider.eth_gasPrice())
# Create the transaction
var tx: TransactionArgs
tx.`from` = Opt.some(accountFrom)
tx.to = Opt.some(tokenAddress)
tx.value = Opt.some(0.u256) # No ETH is sent for token operations
tx.gasPrice = Opt.some(Quantity(gasPrice))
tx.data = Opt.some(byteutils.hexToSeqByte(mintCallData))
debug "Sending mint call"
# Send the transaction
let txHash = await web3.send(tx)
let balanceOfSelector = "0x70a08231"
let balanceCallData = balanceOfSelector & paddedAddress
# Wait a bit for transaction to be mined
await sleepAsync(500.milliseconds)
var balanceCallTx: TransactionArgs
balanceCallTx.to = Opt.some(tokenAddress)
balanceCallTx.data = Opt.some(byteutils.hexToSeqByte(balanceCallData))
# Make the call to get updated balance
let balanceResponse = await web3.provider.eth_call(balanceCallTx, "latest")
let balanceHexStr = "0x" & byteutils.toHex(balanceResponse)
let balanceAfterTokens = stint.parse(balanceHexStr, UInt256, 16)
let balanceAfterExpectedTokens =
recipientBalanceBeforeExpectedTokens.get() + amountTokens
debug "token balance after minting", balanceAfterTokens = balanceAfterTokens
assert balanceAfterTokens == balanceAfterExpectedTokens,
fmt"Token balance is {balanceAfterTokens} but expected {balanceAfterExpectedTokens}"
let balance3 = await web3.provider.eth_getBalance(recipientAddress, "latest")
debug "sendMintCall: after mint recipientAddress account balance: ",
recipientAddress = recipientAddress, balance = balance3
return txHash
proc checkApproval*(
web3: Web3, tokenAddress: Address, owner: Address, spender: Address
): Future[UInt256] {.async.} =
let token = web3.contractSender(ERC20Token, tokenAddress)
let allowance = await token.allowance(owner, spender).call()
debug "Current allowance", owner = owner, spender = spender, allowance = allowance
return allowance
proc sendApproveCall*(
web3: Web3,
accountFrom: Address,
privateKey: keys.PrivateKey,
tokenAddress: Address,
spender: Address,
amountWei: UInt256,
): Future[TxHash] {.async.} =
# # ERC20 approve function signature: approve(address spender, uint256 amount)
# # Create the contract call data
# # Method ID for approve(address,uint256) is 0x095ea7b3
# Temporarily set the private key
let oldPrivateKey = web3.privateKey
web3.privateKey = Opt.some(privateKey)
web3.lastKnownNonce = Opt.none(Quantity) # Reset nonce tracking
# Create approve transaction
let approveSelector = "0x095ea7b3"
let addressHex = spender.toHex() # Already without 0x
let paddedAddress = addressHex.align(64, '0')
let amountHex = amountWei.toHex()
# let amountWithout0x =
# if amountHex.startsWith("0x"):
# amountHex[2 .. ^1]
# else:
# amountHex
let paddedAmount = amountHex.align(64, '0')
let approveCallData = approveSelector & paddedAddress & paddedAmount
# Get gas price
let gasPrice = int(await web3.provider.eth_gasPrice())
# Create the transaction
var tx: TransactionArgs
tx.`from` = Opt.some(accountFrom)
tx.to = Opt.some(tokenAddress)
tx.value = Opt.some(0.u256)
tx.gasPrice = Opt.some(Quantity(gasPrice))
tx.gas = Opt.some(Quantity(100000)) # Add gas limit
tx.data = Opt.some(byteutils.hexToSeqByte(approveCallData))
tx.chainId = Opt.some(Quantity(CHAIN_ID))
debug "Sending approve call with transaction", tx = tx
try:
# Send will automatically sign because privateKey is set
let txHash = await web3.send(tx)
return txHash
except CatchableError as e:
error "Failed to send approve transaction", error = e.msg
raise e
finally:
# Always restore the old private key
web3.privateKey = oldPrivateKey
proc deployTestToken*(
pk: keys.PrivateKey, acc: Address, web3: Web3
): Future[Address] {.async.} =
## Executes a Foundry forge script that deploys the a token contract (ERC-20) used for testing. This is a prerequisite to enable the contract deployment and this token contract address needs to be minted and approved for the accounts that need to register membership with the contract
## submodulePath: path to the submodule containing contract deploy scripts
# All RLN related tests should be run from the root directory of the project
let submodulePath = "./vendor/waku-rlnv2-contract"
let PRIVATE_KEY = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
let forgePath = getForgePath()
debug "Forge path", forgePath
@ -124,7 +267,7 @@ proc deployTestToken*(): Future[string] {.async.} =
# Deploy TestToken contract
let forgeCmdTestToken =
fmt"""cd {submodulePath} && {forgePath} script test/TestToken.sol --broadcast -vv --rpc-url http://localhost:8540 --tc TestTokenFactory --private-key {PRIVATE_KEY} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json"""
fmt"""cd {submodulePath} && {forgePath} script test/TestToken.sol --broadcast -vv --rpc-url http://localhost:8540 --tc TestTokenFactory --private-key {pk} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json"""
let (outputDeployTestToken, exitCodeDeployTestToken) = execCmdEx(forgeCmdTestToken)
trace "Executed forge command to deploy TestToken contract",
output = outputDeployTestToken
@ -144,9 +287,51 @@ proc deployTestToken*(): Future[string] {.async.} =
##TODO: raise exception here?
let testTokenAddress = testTokenAddressRes.get()
debug "Address of the TestToken contract", testTokenAddress
putEnv("TOKEN_ADDRESS", testTokenAddress)
debug "TestToken contract deployer account details", account = acc, pk = pk
return testTokenAddress
let testTokenAddressBytes = hexToByteArray[20](testTokenAddress)
let testTokenAddressAddress = Address(testTokenAddressBytes)
return testTokenAddressAddress
proc approveAndVerify*(
web3: Web3,
accountFrom: Address,
privateKey: keys.PrivateKey,
tokenAddress: Address,
spender: Address,
amountWei: UInt256,
): Future[bool] {.async.} =
debug "Starting approval process",
owner = accountFrom,
tokenAddress = tokenAddress,
spender = spender,
amount = amountWei
# Send approval
let txHash = await sendApproveCall(
web3, accountFrom, privateKey, tokenAddress, spender, amountWei
)
debug "Approval transaction sent", txHash = txHash
# Wait for transaction to be mined
let receipt = await web3.getMinedTransactionReceipt(txHash)
debug "Transaction mined", status = receipt.status, blockNumber = receipt.blockNumber
# Check if status is present and successful
if receipt.status.isNone or receipt.status.get != 1.Quantity:
error "Approval transaction failed"
return false
# Give it a moment for the state to settle
await sleepAsync(100.milliseconds)
# Check allowance after mining
let allowanceAfter = await checkApproval(web3, tokenAddress, accountFrom, spender)
debug "Allowance after approval", amount = allowanceAfter
return allowanceAfter >= amountWei
proc executeForgeContractDeployScripts*(): Future[Address] {.async.} =
## Executes a set of foundry forge scripts required to deploy the RLN contract and returns the deployed proxy contract address
@ -223,10 +408,10 @@ proc executeForgeContractDeployScripts*(): Future[Address] {.async.} =
let wakuRlnV2AddressRes =
getContractAddressFromDeployScriptOutput(outputDeployWakuRln)
if wakuRlnV2AddressRes.isErr():
error "Failed to get LinearPriceCalculator contract address from deploy script output"
error "Failed to get WakuRlnV2 contract address from deploy script output"
##TODO: raise exception here?
let wakuRlnV2Address = wakuRlnV2AddressRes.get()
debug "Address of the LinearPriceCalculator contract", wakuRlnV2Address
debug "Address of the WakuRlnV2 contract", wakuRlnV2Address
putEnv("WAKURLNV2_ADDRESS", wakuRlnV2Address)
# Deploy Proxy contract
@ -321,7 +506,7 @@ proc sendEthTransfer*(
let balanceBeforeWei = await web3.provider.eth_getBalance(accountTo, "latest")
let balanceBeforeExpectedWei = accountToBalanceBeforeExpectedWei.get()
assert balanceBeforeWei == balanceBeforeExpectedWei,
fmt"Balance is {balanceBeforeWei} but expected {balanceBeforeExpectedWei}"
fmt"Balance is {balanceBeforeWei} before transfer but expected {balanceBeforeExpectedWei}"
let gasPrice = int(await web3.provider.eth_gasPrice())
@ -334,17 +519,17 @@ proc sendEthTransfer*(
# TODO: handle the error if sending fails
let txHash = await web3.send(tx)
# Wait a bit for transaction to be mined
await sleepAsync(2000.milliseconds)
if doBalanceAssert:
let balanceAfterWei = await web3.provider.eth_getBalance(accountTo, "latest")
let balanceAfterExpectedWei = accountToBalanceBeforeExpectedWei.get() + amountWei
assert balanceAfterWei == balanceAfterExpectedWei,
fmt"Balance is {balanceAfterWei} but expected {balanceAfterExpectedWei}"
fmt"Balance is {balanceAfterWei} after transfer but expected {balanceAfterExpectedWei}"
return txHash
proc ethToWei(eth: UInt256): UInt256 =
eth * 1000000000000000000.u256
proc createEthAccount*(
ethAmount: UInt256 = 1000.u256
): Future[(keys.PrivateKey, Address)] {.async.} =
@ -449,24 +634,41 @@ proc setupOnchainGroupManager*(
let rlnInstance = rlnInstanceRes.get()
let testTokenAddress = await deployTestToken()
putEnv("TOKEN_ADDRESS", testTokenAddress)
let contractAddress = await executeForgeContractDeployScripts()
# connect to the eth client
let web3 = await newWeb3(ethClientUrl)
let accounts = await web3.provider.eth_accounts()
web3.defaultAccount = accounts[0]
web3.defaultAccount = accounts[1]
let (privateKey, acc) = createEthAccount(web3)
# we just need to fund the default account
# the send procedure returns a tx hash that we don't use, hence discard
discard await sendEthTransfer(
web3, web3.defaultAccount, acc, ethToWei(1000.u256), some(0.u256)
web3, web3.defaultAccount, acc, ethToWei(5000.u256), some(0.u256)
)
let testTokenAddress = await deployTestToken(privateKey, acc, web3)
putEnv("TOKEN_ADDRESS", testTokenAddress.toHex())
discard await sendMintCall(
web3, web3.defaultAccount, testTokenAddress, acc, ethToWei(1000.u256), some(0.u256)
)
let contractAddress = await executeForgeContractDeployScripts()
let approvalSuccess = await approveAndVerify(
web3,
acc, # owner
privateKey,
testTokenAddress, # ERC20 token address
contractAddress, # spender - the proxy contract that will spend the tokens
ethToWei(500.u256),
)
# Also check the token balance
let tokenBalance = await getTokenBalance(web3, testTokenAddress, acc)
debug "Token balance before register", owner = acc, balance = tokenBalance
let manager = OnchainGroupManager(
ethClientUrls: @[ethClientUrl],
ethContractAddress: $contractAddress,