mirror of
https://github.com/logos-messaging/logos-messaging-nim.git
synced 2026-01-03 14:33:12 +00:00
first implementation of token minting and approval
This commit is contained in:
parent
cba55f6fb5
commit
2aa0a64349
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user