From 57a0dba0f1a8968acb6cd237ba37e51bc75bd1bd Mon Sep 17 00:00:00 2001 From: Luke Schoen Date: Mon, 14 Oct 2024 15:47:27 +0200 Subject: [PATCH] WIP --- packages/hardhat/contracts/DummyGateway.sol | 11 +- packages/hardhat/contracts/NunyaBusiness.sol | 25 +- .../nunya-contract/src/contract.rs | 486 ++++++++++++------ .../nunya-contract/src/msg.rs | 69 ++- .../nunya-contract/src/state.rs | 50 +- 5 files changed, 416 insertions(+), 225 deletions(-) diff --git a/packages/hardhat/contracts/DummyGateway.sol b/packages/hardhat/contracts/DummyGateway.sol index 2ea8169..fb21679 100644 --- a/packages/hardhat/contracts/DummyGateway.sol +++ b/packages/hardhat/contracts/DummyGateway.sol @@ -13,13 +13,16 @@ contract SecretContract { function linkPaymentRef(uint256 secret, string calldata ref) external returns (uint256) { return 5; } - function pay(string calldata ref, uint256 amount) external returns (uint256) { + // TODO: `string calldata secret` or `uint256 secret` + function pay(string calldata secret, string calldata ref, uint256 amount, uint256 denomination) external returns (uint256) { return 4; } - function payWithReceipt(string calldata ref, uint256 amount, uint256 userPubkey) external returns (uint256) { + // TODO: `string calldata secret` or `uint256 secret` + function payWithReceipt(string calldata secret, string calldata ref, uint256 amount, uint256 denomination, uint256 userPubkey) external returns (uint256) { return 3; } - function withdraw(string calldata secret, address withdrawalAddress) external returns (uint256){ + // TODO: `string calldata secret` or `uint256 secret` + function withdrawTo(string calldata secret, uint256 amount, uint256 denomination, address withdrawalAddress) external returns (uint256){ return 2; } @@ -27,4 +30,4 @@ contract SecretContract { return 1; } -} \ No newline at end of file +} diff --git a/packages/hardhat/contracts/NunyaBusiness.sol b/packages/hardhat/contracts/NunyaBusiness.sol index 5756836..32b22fe 100644 --- a/packages/hardhat/contracts/NunyaBusiness.sol +++ b/packages/hardhat/contracts/NunyaBusiness.sol @@ -9,9 +9,9 @@ import "hardhat/console.sol"; interface SecretContract { function newSecretUser(uint256 secret) external returns (uint256); function linkPaymentRef(uint256 secret, string calldata ref) external returns (uint256); - function pay(string calldata ref, uint256 amount) external returns (uint256); - function payWithReceipt(string calldata ref, uint256 amount, uint256 userPubkey) external returns (uint256); - function withdraw(string calldata secret, address withdrawalAddress) external returns (uint256); + function pay(string calldata ref, uint256 amount, uint256 denomination) external returns (uint256); + function payWithReceipt(string calldata ref, uint256 amount, uint256 denomination, uint256 userPubkey) external returns (uint256); + function withdrawTo(string calldata secret, uint256 amount, uint256 _denomination, address withdrawalAddress) external returns (uint256); function retrievePubkey() external returns (uint256); } @@ -27,6 +27,7 @@ contract NunyaBusiness { struct Receipt { uint256 paymentRef; uint256 amount; + uint256 denomination; bytes32 sig; } @@ -97,21 +98,23 @@ contract NunyaBusiness { } // TODO: use ref encrypted with (user pubkey+salt) - function pay(string calldata _ref, uint256 _amount) public payable returns (uint256) { + // TODO: `string calldata secret` or `uint256 secret` + function pay(string calldata _secret, string calldata _ref, uint256 _amount, uint256 _denomination) public payable returns (uint256) { // >= because we need gas for require (_amount >= msg.value, "Naughty!"); uint256 gasPaid = fundGateway(); - uint256 requestId = secretContract.pay(_ref, msg.value-gasPaid); + uint256 requestId = secretContract.pay(_secret, _ref, msg.value-gasPaid, _denomination); expectedResult[requestId]=FunctionCallType.PAY; return(requestId); } // TODO: use ref encrypted with (user pubkey+salt) - function pay(string calldata _ref, uint256 _amount, uint256 _userPubkey) public payable returns (uint256) { + // TODO: `string calldata secret` or `uint256 secret` + function pay(string calldata _secret, string calldata _ref, uint256 _amount, uint256 _denomination, uint256 _userPubkey) public payable returns (uint256) { // >= because we need gas for require (_amount >= msg.value, "Naughty!"); uint256 gasPaid = fundGateway(); - uint256 requestId = secretContract.payWithReceipt(_ref, msg.value-gasPaid, _userPubkey); + uint256 requestId = secretContract.payWithReceipt(_secret, _ref, msg.value-gasPaid, _denomination, _userPubkey); expectedResult[requestId]=FunctionCallType.PAY; return(requestId); } @@ -155,20 +158,22 @@ contract NunyaBusiness { } // Function wrapped in secret network payload encryption - function withdrawTo(string calldata _secret, uint256 _amount, address _withdrawalAddress) public payable returns (uint256) { + // TODO: `string calldata secret` or `uint256 secret` + function withdrawTo(string calldata _secret, uint256 _amount, uint256 _denomination, address _withdrawalAddress) public payable returns (uint256) { require((_amount > 0), "Account not found or empty."); fundGateway(msg.value); - uint256 requestId = secretContract.withdraw(_secret, _withdrawalAddress); + uint256 requestId = secretContract.withdrawTo(_secret, _amount, _denomination, _withdrawalAddress); // TODO: error check expectedResult[requestId]=FunctionCallType.WITHDRAW; return(requestId); } - function withdrawToCallback(uint256 _requestId, bool _success, uint256 _amount, address payable _withdrawalAddress) onlyGateway public { + function withdrawToCallback(uint256 _requestId, bool _success, uint256 _amount, uint256 _denomination, address payable _withdrawalAddress) onlyGateway public { require (expectedResult[_requestId]==FunctionCallType.WITHDRAW); if (!_success) emit SecretNetworkError(_requestId, "Error withdrawing - out of funds?"); require(_amount > 0, "Account not found or empty."); + // TODO: only if the `_denomination` is "ETH"? _withdrawalAddress.transfer(_amount); emit RequestSuccess(_requestId); } diff --git a/packages/secret-contracts/nunya-contract/src/contract.rs b/packages/secret-contracts/nunya-contract/src/contract.rs index 12d31c8..100432c 100644 --- a/packages/secret-contracts/nunya-contract/src/contract.rs +++ b/packages/secret-contracts/nunya-contract/src/contract.rs @@ -1,18 +1,19 @@ use crate::{ msg::{ ExecuteMsg, GatewayMsg, InstantiateMsg, QueryMsg, - NewSecretUserStoreMsg, LinkPaymentRefStoreMsg, PayStoreMsg, PayEncryptedWithReceiptStoreMsg, WithdrawToStoreMsg, - ResponseStoreMsg, ResponseRetrievePubkeyMsg, + NewAuthOutStoreMsg, LinkPaymentRefStoreMsg, PayStoreMsg, WithdrawToStoreMsg, + ResponseNewAuthOutStoreMsg, ResponseLinkPaymentRefStoreMsg, ResponsePayStoreMsg, ResponseWithdrawToStoreMsg, ResponseRetrievePubkeyMsg, }, state::{ - NewSecretUser, LinkPaymentRef, Pay, PayEncryptedWithReceipt, WithdrawTo, + PaymentReceipt, PaymentReferenceBalance, ResponseStatusCode, State, CONFIG, - PUBKEY_MAP, NEW_SECRET_USER_MAP, LINK_PAYMENT_REF_MAP, PAY_MAP, PAY_ENCRYPTED_WITH_RECEIPT_MAP, WITHDRAW_TO_MAP, + VIEWING_KEY, VIEWING_KEY_TO_BALANCE_MAP, VIEWING_KEY_TO_PAYMENT_REF_MAP, }, }; use cosmwasm_std::{ - entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Uint128, Uint256 + entry_point, to_binary, coin, Binary, Deps, DepsMut, Env, HumanAddr, MessageInfo, Response, StdError, StdResult, Uint128, Uint256 }; +use secret_toolkit::viewing_key::{ViewingKey, ViewingKeyStore}; use secret_toolkit::utils::{pad_handle_result, pad_query_result, HandleCallback}; use tnls::{ msg::{PostExecutionMsg, PrivContractHandleMsg}, @@ -30,6 +31,7 @@ pub fn instantiate( _info: MessageInfo, msg: InstantiateMsg, ) -> StdResult { + // Initialise state let state = State { gateway_address: msg.gateway_address, gateway_hash: msg.gateway_hash, @@ -38,6 +40,12 @@ pub fn instantiate( CONFIG.save(deps.storage, &state)?; + // let msg = InstantiateMsg { + // gateway_address: gateway.to_string(), + // // TODO: + // secret_contract_pubkey: 1, + // }; + Ok(Response::default()) } @@ -59,6 +67,8 @@ fn try_handle( // verify signature with stored gateway public key let config = CONFIG.load(deps.storage)?; + // Security + // // reference: evm-kv-store-demo if info.sender != config.gateway_address { return Err(StdError::generic_err( @@ -77,17 +87,19 @@ fn try_handle( // determine which function to call based on the included handle let handle = msg.handle.as_str(); match handle { - "newSecretUser" => create_new_secret_user(deps, env, msg.input_values, msg.task, msg.input_hash), + "newSecretUser" => create_new_auth_out(deps, env, msg.input_values, msg.task, msg.input_hash), "linkPaymentRef" => create_link_payment_ref(deps, env, msg.input_values, msg.task, msg.input_hash), + // handle both `pay` and `payWithReceipt` Solidity function calls using the same `create_pay` Secret contract function "pay" => create_pay(deps, env, msg.input_values, msg.task, msg.input_hash), - "payWithReceipt" => create_pay_encrypted_with_receipt(deps, env, msg.input_values, msg.task, msg.input_hash), + "payWithReceipt" => create_pay(deps, env, msg.input_values, msg.task, msg.input_hash), "withdrawTo" => create_withdraw_to(deps, env, msg.input_values, msg.task, msg.input_hash), + // "setSecretContractPubkey" => create_secret_contract_pubkey(deps, env, msg.input_values, msg.task, msg.input_hash), _ => Err(StdError::generic_err("invalid handle".to_string())), } } -fn create_new_secret_user( +fn create_new_auth_out( deps: DepsMut, env: Env, input_values: String, @@ -96,30 +108,58 @@ fn create_new_secret_user( ) -> StdResult { let config = CONFIG.load(deps.storage)?; - let input: NewSecretUserStoreMsg = serde_json_wasm::from_str(&input_values) + let input: NewAuthOutStoreMsg = serde_json_wasm::from_str(&input_values) .map_err(|err| StdError::generic_err(err.to_string()))?; - let secret_user = input.secret_user; - // .map_err(|err| StdError::generic_err(format!("Invalid _secret: {}", err)))?; + let viewing_key_index = input.secret_user.as_str(); // convert u8 to String + + assert!(viewing_key_index.chars().count() > 0, Err(StdError::generic_err("Secret must not be an empty string"))); + + // https://docs.scrt.network/secret-network-documentation/development/development-concepts/permissioned-viewing/viewing-keys#viewing-keys-introduction + // https://github.com/scrtlabs/examples/blob/master/secret-viewing-keys/secret-viewing-keys-contract/src/contract.rs + let viewing_key = ViewingKey::create(deps.storage, &info, &env, &gateway_account, b"entropy"); + + // https://docs.scrt.network/secret-network-documentation/development/development-concepts/permissioned-viewing/viewing-keys#viewing-keys-implementation + let gateway_account = config.gateway_address.to_string(); + let suffix = viewing_key_index.to_string(); + let index_concat = gateway_account.push_str(suffix); + + // Viewing Key + VIEWING_KEY + // TODO - sender is always the gateway contract, or perhaps change this to `info.sender.as_bytes()` + .add_suffix(config.gateway_address.to_string()) + .insert(deps.storage, &viewing_key_index, &viewing_key)?; - let new_secret_user = NewSecretUser { - secret_user: secret_user + // Attempt to retrieve existing + let value_payment_reference_to_balances_map = VIEWING_KEY_TO_PAYMENT_REF_TO_BALANCES_MAP + .get(deps.storage, &index_concat) + .ok_or_else(|| StdError::generic_err("Value for this VIEWING_KEY_TO_PAYMENT_REF_TO_BALANCES_MAP key not found"))?; + + // TODO: need to setup viewing key for the mapping, but not necessary to store this example + let init_balance: Coin = coin(0u128, String::new()); + let init_payment_ref = String::new(); + let init_payment_reference_balance = PaymentReferenceBalance { + payment_reference: init_payment_ref, + balance: init_balance, + }; + + let mut value_payment_reference_to_balances: Vec = match value_payment_reference_to_balances_map { + Some(payment_reference_to_balances) => payment_reference_to_balances, // If there are existing + None => Vec::new(), // If none are found, start with an empty vector }; - // Extract KeyIter from Result, handle error if necessary - let key_iter_result = NEW_SECRET_USER_MAP.iter_keys(deps.storage); - let mut max_key: u32 = 0; + // Add the new to vector + value_payment_reference_to_balances.push(init_payment_reference_balance.clone()); - for key in key_iter_result? { - max_key = max_key.max(key?); - } - let new_key = max_key + 1; + // Save updated back to storage + VIEWING_KEY_TO_PAYMENT_REF_TO_BALANCES_MAP + .insert(deps.storage, &viewing_key, &value_payment_reference_to_balances)?; - // Insert the new item with the new key - NEW_SECRET_USER_MAP.insert(deps.storage, &new_key, &new_secret_user)?; + let response_status_code: ResponseStatusCode = 0u16; - let data = ResponseStoreMsg { - message: true, + let data = ResponseNewAuthOutStoreMsg { + _requestId: task, + _code: response_status_code, }; let json_string = @@ -128,9 +168,15 @@ fn create_new_secret_user( let result = base64::encode(json_string); let callback_msg = GatewayMsg::Output { + // Sepolia network gateway contract Solidity source code + // https://github.com/SecretSaturn/SecretPath/blob/main/TNLS-Gateways/public-gateway/src/Gateway.sol + // Sepolia network gateway contract deployed code (line 383, 914) + // https://sepolia.etherscan.io/address/0x0B6c705db59f7f02832d66B97b03E9EB3c0b4AAB#code + // Secret network gateway contract Rust source code `PostExecutionInfo` + // https://github.com/SecretSaturn/SecretPath/blob/main/TNLS-Gateways/solana-gateway/programs/solana-gateway/src/lib.rs#L737 outputs: PostExecutionMsg { result, - task, + task, // task is the requestId input_hash, }, } @@ -142,7 +188,7 @@ fn create_new_secret_user( Ok(Response::new() .add_message(callback_msg) - .add_attribute("status", "stored value with key")) + .add_attribute("status", "create_new_auth_out")) } fn create_link_payment_ref( @@ -157,34 +203,63 @@ fn create_link_payment_ref( let input: LinkPaymentRefStoreMsg = serde_json_wasm::from_str(&input_values) .map_err(|err| StdError::generic_err(err.to_string()))?; - let secret_user = input - .secret_user; - // .map_err(|err| StdError::generic_err(format!("Invalid _secret: {}", err)))?; + let viewing_key_index = input.secret_user.as_str(); // convert u8 to String - let payment_ref = input - .payment_ref - .parse::() - .map_err(|err| StdError::generic_err(format!("Invalid _ref: {}", err)))?; + assert!(viewing_key_index.chars().count() > 0, Err(StdError::generic_err("Secret must not be an empty string"))); - let link_payment_reference = LinkPaymentRef { - secret_user: secret_user, - payment_ref: payment_ref, - }; + // https://docs.scrt.network/secret-network-documentation/development/development-concepts/permissioned-viewing/viewing-keys#viewing-keys-implementation + let gateway_account = config.gateway_address.to_string(); + let suffix = viewing_key_index.to_string(); + let index_concat = gateway_account.push_str(suffix); + + let result = ViewingKey::check(&deps.storage, &index_concat, b"entropy"); + assert_neq!(result, Err(StdError::generic_err("unauthorized"))); - // Extract KeyIter from Result, handle error if necessary - let key_iter_result = LINK_PAYMENT_REF_MAP.iter_keys(deps.storage); - let mut max_key: u32 = 0; + let value_viewing_key = VIEWING_KEY + .get(deps.storage, &index_concat) + .ok_or_else(|| StdError::generic_err("Value for this VIEWING_KEY key not found"))?; - for key in key_iter_result? { - max_key = max_key.max(key?); + if value_viewing_key != index_concat { + return Err(StdError::generic_err("Viewing Key incorrect or not found")); } - let new_key = max_key + 1; - // Insert the item with the new key - LINK_PAYMENT_REF_MAP.insert(deps.storage, &new_key, &link_payment_reference)?; + let payment_ref = input.payment_ref.as_str(); // convert Uint256 to String + + assert!(payment_ref.chars().count() > 0, Err(StdError::generic_err("Payment reference must not be an empty string"))); + + // TODO: Check stored correctly but move to tests + let value_payment_reference_to_balances = VIEWING_KEY_TO_PAYMENT_REF_TO_BALANCES_MAP + .get(deps.storage, &index_concat) + .ok_or_else(|| StdError::generic_err("Value for this VIEWING_KEY_TO_PAYMENT_REF_TO_BALANCES_MAP key not found"))?; + + // TODO: if payment_ref already exists in VIEWING_KEY_TO_PAYMENT_REF_TO_BALANCES_MAP value then early exit here + + let new_balance: Coin = coin(0u128, String::new()); + let new_payment_ref = payment_ref; + let new_payment_reference_balance: PaymentReferenceBalance = { + payment_reference: new_payment_ref, + balance: new_balance, + }; + + let mut value_payment_reference_to_balances: Vec = match value_payment_reference_to_balances_map { + Some(payment_reference_to_balances) => payment_reference_to_balances, // If there are existing + None => Vec::new(), // If none are found, start with an empty vector + }; + + // Add the new to vector + value_payment_reference_to_balances.push(new_payment_reference_balance.clone()); + + // Save updated back to storage + VIEWING_KEY_TO_PAYMENT_REF_TO_BALANCES_MAP + .insert(deps.storage, &viewing_key, &value_payment_reference_to_balances)?; + + + let response_status_code: ResponseStatusCode = 0u16; - let data = ResponseStoreMsg { - message: true, + let data = ResponseLinkPaymentRefStoreMsg { + _requestId: task, + _code: response_status_code, + _reference: value_payment_ref.as_str(), }; let json_string = @@ -207,7 +282,7 @@ fn create_link_payment_ref( Ok(Response::new() .add_message(callback_msg) - .add_attribute("status", "stored value with key")) + .add_attribute("status", "create_link_payment_ref")) } fn create_pay( @@ -222,107 +297,109 @@ fn create_pay( let input: PayStoreMsg = serde_json_wasm::from_str(&input_values) .map_err(|err| StdError::generic_err(err.to_string()))?; - let payment_ref = input - .payment_ref - .parse::() - .map_err(|err| StdError::generic_err(format!("Invalid _ref: {}", err)))?; + let viewing_key_index = input.secret_user.as_str(); // convert u8 to String + let payment_ref = input.payment_ref.as_str(); // convert Uint256 to String + // TODO: handle error if issue with amount or denomination received + let amount: Uint128 = input.amount.into(); // Uint128 + let denomination = input.denomination.as_str(); - let amount = input - .amount; - // .parse::() - // .map_err(|err| StdError::generic_err(format!("Invalid _amount: {}", err)))?; + assert!(viewing_key_index.chars().count() > 0, Err(StdError::generic_err("Secret must not be an empty string"))); + assert!(payment_ref.chars().count() > 0, Err(StdError::generic_err("Payment reference must not be an empty string"))); + assert!(amount >= 0u128, Err(StdError::generic_err("Payment amount must be greater than 0"))); + assert!(denomination.chars().count() > 0, Err(StdError::generic_err("Payment denomination must not be an empty string"))); - let pay = Pay { - payment_ref: payment_ref, - amount: amount, - }; + // https://docs.scrt.network/secret-network-documentation/development/development-concepts/permissioned-viewing/viewing-keys#viewing-keys-implementation + let gateway_account = config.gateway_address.to_string(); + let suffix = viewing_key_index.to_string(); + let index_concat = gateway_account.push_str(suffix); + + let result = ViewingKey::check(&deps.storage, &index_concat, b"entropy"); + assert_neq!(result, Err(StdError::generic_err("unauthorized"))); - // Extract KeyIter from Result, handle error if necessary - let key_iter_result = PAY_MAP.iter_keys(deps.storage); - let mut max_key: u32 = 0; + let value_viewing_key = VIEWING_KEY + .get(deps.storage, &index_concat) + .ok_or_else(|| StdError::generic_err("Value for this VIEWING_KEY key not found"))?; - for key in key_iter_result? { - max_key = max_key.max(key?); + if value_viewing_key != index_concat { + return Err(StdError::generic_err("Viewing Key incorrect or not found")); } - let new_key = max_key + 1; - // Insert the item with the new key - PAY_MAP.insert(deps.storage, &new_key, &pay)?; + // Attempt to retrieve existing + let value_payment_reference_to_balances_map = VIEWING_KEY_TO_PAYMENT_REF_TO_BALANCES_MAP + .get(deps.storage, &index_concat) + .ok_or_else(|| StdError::generic_err("Value for this VIEWING_KEY_TO_PAYMENT_REF_TO_BALANCES_MAP key not found"))?; - let data = ResponseStoreMsg { - message: true, + let mut value_payment_reference_to_balances: Vec = match value_payment_reference_to_balances_map { + Some(payment_reference_to_balances) => payment_reference_to_balances, // If there are existing + None => StdError::generic_err("No payment references found"), // If none are found, early return }; - let json_string = - serde_json_wasm::to_string(&data).map_err(|err| StdError::generic_err(err.to_string()))?; - - let result = base64::encode(json_string); + // Check if matching `payment_ref` is in the vector since only pay if it exists + let index: usize = value_payment_reference_to_balances.iter().position(|&r| r.payment_reference == payment_ref).unwrap(); - let callback_msg = GatewayMsg::Output { - outputs: PostExecutionMsg { - result, - task, - input_hash, + let value_payment_reference_to_balances_match = match index { + Some(val) => { + println!("Found matching payment reference at index: {:#?}", index); + val }, - } - .to_cosmos_msg( - config.gateway_hash, - config.gateway_address.to_string(), - None, - )?; - - Ok(Response::new() - .add_message(callback_msg) - .add_attribute("status", "stored value with key")) -} - -fn create_pay_encrypted_with_receipt( - deps: DepsMut, - env: Env, - input_values: String, - task: Task, - input_hash: Binary, -) -> StdResult { - let config = CONFIG.load(deps.storage)?; - - let input: PayEncryptedWithReceiptStoreMsg = serde_json_wasm::from_str(&input_values) - .map_err(|err| StdError::generic_err(err.to_string()))?; - - let payment_ref = input - .payment_ref - .parse::() - .map_err(|err| StdError::generic_err(format!("Invalid _ref: {}", err)))?; - - let amount = input - .amount; - // .parse::() - // .map_err(|err| StdError::generic_err(format!("Invalid _amount: {}", err)))?; - - let user_pubkey = input - .user_pubkey; - // .parse::() - // .map_err(|err| StdError::generic_err(format!("Invalid _userPubkey: {}", err)))?; - - let pay_encrypted_with_receipt = PayEncryptedWithReceipt { - payment_ref: payment_ref, - amount: amount, - user_pubkey: user_pubkey + None => StdError::generic_err("No payment references found"), }; - // Extract KeyIter from Result, handle error if necessary - let key_iter_result = PAY_ENCRYPTED_WITH_RECEIPT_MAP.iter_keys(deps.storage); - let mut max_key: u32 = 0; + // Add pay amount to existing balance associated with the payment reference that was found + let new_balance_amount: Uint128 = value_payment_reference_to_balances_match.balance.amount.saturating_add(amount); + let new_balance: Coin = coin(&new_balance_amount, &denomination); + let new_payment_reference_balance = PaymentReferenceBalance { + payment_reference: payment_ref, + balance: new_balance, + }; - for key in key_iter_result? { - max_key = max_key.max(key?); + // Update the index in the vector with the matching payment reference + value_payment_reference_to_balances[index] = new_payment_reference_balance; + + // Save updated back to storage + VIEWING_KEY_TO_PAYMENT_REF_TO_BALANCES_MAP + .insert(deps.storage, &viewing_key, &value_payment_reference_to_balances)?; + + let mut receipt: Option = None; + + // FIXME: sign receipt using Secret contract's private key, currently just hardcoded. + // But how to get the Secret contract's public and private key? + let signature: bytes32 = "0x".as_bytes(); + + let user_pubkey: Uint256; + if let Some(ref _user_pubkey) = input.user_pubkey { + user_pubkey = _user_pubkey; + + // TODO: if user_pubkey has been provided then return encrypted receipt and signature with the user_pubkey + // TODO: still need to encrypt below with user_pubkey + receipt = Some({ + payment_reference: value_payment_ref, + // TODO: convert Uint256 to Uint128 + // Note: denomination of `Coin` type for the Receipt, since that is handled by the Solidity contract + amount: new_balance_amount, + // TODO: serialise denomination + denomination: denomination, + sig: signature, + }); + } else { + // return receipt and signature unencrypted + receipt = Some({ + payment_reference: value_payment_ref, + // TODO: convert Uint256 to Uint128 + // Note: denomination of `Coin` type for the Receipt, since that is handled by the Solidity contract + amount: new_balance_amount, + // TODO: serialise denomination + denomination: denomination, + sig: signature, + }); } - let new_key = max_key + 1; - // Insert the item with the new key - PAY_ENCRYPTED_WITH_RECEIPT_MAP.insert(deps.storage, &new_key, &pay_encrypted_with_receipt)?; + let response_status_code: ResponseStatusCode = 0u16; - let data = ResponseStoreMsg { - message: true, + let data = ResponsePayStoreMsg { + _requestId: task, + _code: response_status_code, + _receipt: receipt, }; let json_string = @@ -345,7 +422,7 @@ fn create_pay_encrypted_with_receipt( Ok(Response::new() .add_message(callback_msg) - .add_attribute("status", "stored value with key")) + .add_attribute("status", "create_pay")) } fn create_withdraw_to( @@ -360,39 +437,73 @@ fn create_withdraw_to( let input: WithdrawToStoreMsg = serde_json_wasm::from_str(&input_values) .map_err(|err| StdError::generic_err(err.to_string()))?; - let secret_user = input - .secret_user; - // .map_err(|err| StdError::generic_err(format!("Invalid _secret: {}", err)))?; + let viewing_key_index = input.secret_user.as_str(); // convert u8 to String - let amount = input - .amount; - // .parse::() - // .map_err(|err| StdError::generic_err(format!("Invalid amount: {}", err)))?; + // Do not include receiving any payment reference since want to ignore it - let withdrawal_address = input - .withdrawal_address; - // .map_err(|err| StdError::generic_err(format!("Invalid withdrawalAddress: {}", err)))?; + // TODO: handle error if issue with amount or denomination received + let amount: Uint128 = input.amount.into(); // Uint128 + let denomination = input.denomination.as_str(); + let withdrawal_address: [u8; 20] = input.withdrawal_address.into(); // or `Addr` - let withdrawal_to = WithdrawTo { - secret_user: secret_user, - amount: amount, - withdrawal_address: withdrawal_address - }; + assert!(viewing_key_index.chars().count() > 0, Err(StdError::generic_err("Secret must not be an empty string"))); + // Do not validate payment reference since want to ignore it + assert!(amount >= 0u128, Err(StdError::generic_err("Payment amount must be greater than 0"))); + assert!(denomination.chars().count() > 0, Err(StdError::generic_err("Payment denomination must not be an empty string"))); + // TODO: validate withdrawal address input. check if could be `Addr` type + + // https://docs.scrt.network/secret-network-documentation/development/development-concepts/permissioned-viewing/viewing-keys#viewing-keys-implementation + let gateway_account = config.gateway_address.to_string(); + let suffix = viewing_key_index.to_string(); + let index_concat = gateway_account.push_str(suffix); + + let result = ViewingKey::check(&deps.storage, &index_concat, b"entropy"); + assert_neq!(result, Err(StdError::generic_err("unauthorized"))); - // Extract KeyIter from Result, handle error if necessary - let key_iter_result = WITHDRAW_TO_MAP.iter_keys(deps.storage); - let mut max_key: u32 = 0; + let value_viewing_key = VIEWING_KEY + .get(deps.storage, &index_concat) + .ok_or_else(|| StdError::generic_err("Value for this VIEWING_KEY key not found"))?; - for key in key_iter_result? { - max_key = max_key.max(key?); + if value_viewing_key != index_concat { + return Err(StdError::generic_err("Viewing Key incorrect or not found")); } - let new_key = max_key + 1; - // Insert the item with the new key - WITHDRAW_TO_MAP.insert(deps.storage, &new_key, &withdrawal_to)?; + // Attempt to retrieve existing + let value_payment_reference_to_balances_map = VIEWING_KEY_TO_PAYMENT_REF_TO_BALANCES_MAP + .get(deps.storage, &index_concat) + .ok_or_else(|| StdError::generic_err("Value for this VIEWING_KEY_TO_PAYMENT_REF_TO_BALANCES_MAP key not found"))?; - let data = ResponseStoreMsg { - message: true, + let mut value_payment_reference_to_balances: Vec = match value_payment_reference_to_balances_map { + Some(payment_reference_to_balances) => payment_reference_to_balances, // If there are existing + None => StdError::generic_err("No payment references found"), // If none are found, early return + }; + + // Do not only withdraw associated with a specific payment reference at an index in the storage vector since want to ignore it + // Do not need to check that the `payment_ref` provided exists in storage since ignore it + // Do not need to check using the `payment_ref` that we want to withdraw using: + // the `amount` provided ensuring it is less than or equal to the amount stored associated with the payment reference + // and that the `denomination` matches the provided denomination that we want to withdraw + + // Do not store withdrawal address in state + // Do not transfer anything on Secret Network + // Only authorise the withdrawal + + let mut balance_all_payment_refs: Uint128 = 0u128; + for element in value_payment_reference_to_balances.into_iter() { + if element.balance.denom == denomination { + balance_all_payment_refs += element.balance.amount + } + } + + assert!(balance_all_payment_refs >= amount, Err(StdError::generic_err("Withdrawal amount must be less than or equal to total balance of all payment references"))); + + let response_status_code: ResponseStatusCode = 0u16; + + let data = ResponseWithdrawToStoreMsg { + _requestId: task, + _code: response_status_code, + _amount: amount, + _withdrawalAddress: withdrawal_address, }; let json_string = @@ -415,7 +526,7 @@ fn create_withdraw_to( Ok(Response::new() .add_message(callback_msg) - .add_attribute("status", "stored value with key")) + .add_attribute("status", "DO_THE_WITHDRAWAL")) } #[entry_point] @@ -426,12 +537,61 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { pad_query_result(response, BLOCK_SIZE) } +// TODO: remove `key: u32` fn retrieve_pubkey_query(deps: Deps, key: u32) -> StdResult { - let value = PUBKEY_MAP - .get(deps.storage, &key) - .ok_or_else(|| StdError::generic_err("Value not found"))?; + let config = CONFIG.load(deps.storage)?; + + let value: [u8; 32] = config.secret_contract_pubkey; to_binary(&ResponseRetrievePubkeyMsg { - message: true, + _key: value, }) } + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::{message_info, mock_dependencies, mock_dependencies_with_balances, mock_env}; + use cosmwasm_std::coins; + use cosmwasm_std::{from_binary, StdError}; + + #[test] + fn proper_initialization() { + let mut deps = mock_dependencies(); + // Create some Addr instances for testing + let gateway = deps.api.addr_make("gateway"); + // https://docs.rs/cosmwasm-std/latest/cosmwasm_std/testing/fn.message_info.html + let msg = InstantiateMsg { + gateway_address: gateway.to_string(), + secret_contract_pubkey: 1, + }; + + let info = message_info(&gateway, &coins(1000, "earth")); + // we can just call .unwrap() to assert this was a success + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + assert_eq!(0, res.messages.len()); + + // it worked, let's query the state + let res = query(deps.as_ref(), mock_env(), QueryMsg::RetrievePubkey {}).unwrap(); + let value: PubkeyResponse = from_binary(&res).unwrap(); + assert_eq!(1, value.secret_contract_pubkey); + } + + // // reference: SecretNetwork codebase + // #[test] + // fn querier_callbacks_work() { + // // Secret contract address + // let rich_addr = HumanAddr::from("foobar"); + // let rich_balance = coins(10000, "gold"); + // let deps = mock_dependencies_with_balances(20, &[(&rich_addr, &rich_balance)]); + + // // querying with balance gets the balance + // let bal = query_other_balance(&deps, rich_addr).unwrap(); + // assert_eq!(bal.amount, rich_balance); + + // // querying other accounts gets none + // let bal = query_other_balance(&deps, HumanAddr::from("someone else")).unwrap(); + // assert_eq!(bal.amount, vec![]); + // } +} diff --git a/packages/secret-contracts/nunya-contract/src/msg.rs b/packages/secret-contracts/nunya-contract/src/msg.rs index 0eec5bc..f11e3c9 100644 --- a/packages/secret-contracts/nunya-contract/src/msg.rs +++ b/packages/secret-contracts/nunya-contract/src/msg.rs @@ -1,15 +1,23 @@ use cosmwasm_std::{Addr, Binary, Uint128, Uint256}; use secret_toolkit::utils::HandleCallback; use tnls::msg::{PostExecutionMsg, PrivContractHandleMsg}; +use tnls::state::{Task} use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::{ + state::{ + ResponseStatusCode, + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct InstantiateMsg { pub gateway_address: Addr, pub gateway_hash: String, pub gateway_key: Binary, + pub secret_contract_pubkey: [u8; 32], } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -19,27 +27,25 @@ pub enum ExecuteMsg { } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct NewSecretUserStoreMsg { - pub secret_user: Addr, +pub struct NewAuthOutStoreMsg { + // TODO - is this key the variable of the parameter provided from Solidity contract to its new_secret_user function? + // secret_user or auth_out + pub secret_user: String, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct LinkPaymentRefStoreMsg { - pub secret_user: Addr, + pub secret_user: String, pub payment_ref: String, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct PayStoreMsg { + pub secret_user: String, pub payment_ref: String, pub amount: Uint128, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct PayEncryptedWithReceiptStoreMsg { - pub payment_ref: String, - pub amount: Uint128, - pub user_pubkey: Uint256, + pub denomination: Uint256, + pub user_pubkey: Option, // encrypted with receipt } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -50,9 +56,32 @@ pub struct WithdrawToStoreMsg { } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct ResponseStoreMsg { - // response message - pub message: bool, +pub struct ResponseNewAuthOutStoreMsg { + pub _requestId: Task, + pub _code: ResponseStatusCode, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct ResponseLinkPaymentRefStoreMsg { + pub _requestId: Task, + pub _code: ResponseStatusCode, + pub _reference: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct ResponsePayStoreMsg { + pub _requestId: Task, + pub _code: ResponseStatusCode, + pub _receipt: Receipt, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct ResponseWithdrawToStoreMsg { + pub _requestId: Task, + pub _code: ResponseStatusCode, + pub _amount: Uint128, + // TODO: should this be of type `Addr`? does it support EVM addresses? + pub _withdrawalAddress: Addr, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -63,16 +92,24 @@ pub enum QueryMsg { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct ResponseRetrievePubkeyMsg { - // response message - pub message: bool, + // TODO: can only access `_requestId` if function called from `try_handle` and callback, but not from queries + // pub _requestId: Uint256, + pub _key: Uint256, } -// TODO: this may not be necessary +// TODO: this may not be necessary as not used #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct QueryResponse { pub message: String, } +// We define a custom struct for each query response +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +pub struct PubkeyResponse { + pub secret_contract_pubkey: [u8; 32], +} + + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum GatewayMsg { diff --git a/packages/secret-contracts/nunya-contract/src/state.rs b/packages/secret-contracts/nunya-contract/src/state.rs index 7d71d1d..8077a35 100644 --- a/packages/secret-contracts/nunya-contract/src/state.rs +++ b/packages/secret-contracts/nunya-contract/src/state.rs @@ -1,51 +1,37 @@ -use cosmwasm_std::{Addr, Binary, Uint128, Uint256}; +use cosmwasm_std::{Addr, Binary, Coin, Uint128, Uint256}; use secret_toolkit::storage::{Item, Keymap}; +use secret_toolkit::viewing_key::{ViewingKey, ViewingKeyStore}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; pub static CONFIG: Item = Item::new(b"config"); -pub static NEW_SECRET_USER_MAP: Keymap = Keymap::new(b"NEW_SECRET_USER_MAP"); -pub static PUBKEY_MAP: Keymap> = Keymap::new(b"PUBKEY_MAP"); -pub static LINK_PAYMENT_REF_MAP: Keymap> = Keymap::new(b"LINK_PAYMENT_REF_MAP"); -pub static PAY_MAP: Keymap> = Keymap::new(b"PAY_MAP"); -pub static PAY_ENCRYPTED_WITH_RECEIPT_MAP: Keymap> = Keymap::new(b"PAY_ENCRYPTED_WITH_RECEIPT_MAP"); -pub static WITHDRAW_TO_MAP: Keymap> = Keymap::new(b"WITHDRAW_TO_MAP"); +pub static VIEWING_KEY: Keymap = Keymap::new(b"VIEWING_KEY"); +pub static VIEWING_KEY_TO_PAYMENT_REF_TO_BALANCES_MAP: Keymap> = Keymap::new(b"VIEWING_KEY_TO_PAYMENT_REF_TO_BALANCES_MAP"); + +pub Index: u8; +pub ViewingKey: String; +pub ContractAddress: [u8; 32]; +pub ResponseStatusCode: u16; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct State { pub gateway_address: Addr, pub gateway_hash: String, pub gateway_key: Binary, + pub secret_contract_pubkey: [u8; 32], } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct NewSecretUser { - pub secret_user: Addr, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct LinkPaymentRef { - pub secret_user: Addr, - pub payment_ref: String, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct Pay { - pub payment_ref: String, - pub amount: Uint128, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct PayEncryptedWithReceipt { - pub payment_ref: String, - pub amount: Uint128, - pub user_pubkey: Uint256, +pub struct PaymentReferenceBalance { + pub payment_reference: String, + pub balance: Coin, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct WithdrawTo { - pub secret_user: Addr, - pub amount: Uint128, - pub withdrawal_address: Addr, +struct PaymentReceipt { + pub payment_reference: Uint256, + pub amount: Uint256, + pub denomination: Uint256, + pub sig: bytes32, }