diff --git a/sponsoredTransactionsAuction/frontend/src/auction_contract.tsx b/sponsoredTransactionsAuction/frontend/src/auction_contract.tsx index 6e5610a5..aacad357 100644 --- a/sponsoredTransactionsAuction/frontend/src/auction_contract.tsx +++ b/sponsoredTransactionsAuction/frontend/src/auction_contract.tsx @@ -1,4 +1,4 @@ -import * as AuctionContract from '../generated/sponsored_tx_enabled_auction_sponsored_tx_enabled_auction'; // Code generated from a smart contract module. +import * as AuctionContract from '../generated/sponsored_tx_enabled_auction_sponsored_tx_enabled_auction'; // Code generated from a smart contract module. The naming convention of the generated file is `moduleName_smartContractName`. import { AccountTransactionType, @@ -34,14 +34,15 @@ const contract = AuctionContract.createUnchecked( ContractAddress.create(Number(process.env.AUCTION_CONTRACT_INDEX), CONTRACT_SUB_INDEX), ); -// This function submits a transaction to add an item to the auction contract. +export const AUCTION_CONTRACT = contract; + /** - * Add new item to the auction contract. + * This function submits a transaction to add an item to the auction contract. * - * @param connection - The wallet connection to use for sending the transaction - * @param accountAddress - The account address to send from - * @param addItemParameter - The parameter for the add item function - * @throws If the contract could not be updated + * @param connection - The wallet connection to use for sending the transaction. + * @param accountAddress - The account address to send from. + * @param addItemParameter - The parameter for the addItem function. + * @throws If simulating the contract update fails. * @returns A promise resolving with the corresponding {@linkcode TransactionHash.Type} */ export async function addItem( @@ -63,11 +64,11 @@ export async function addItem( ); } - const maxContractExecutionEnergy = Energy.create(dryRunResult.usedEnergy.value + EPSILON_ENERGY); // + EPSILON_ENERGY needs to be here, as there seems to be an issue with running out of energy 1 energy prior to reaching the execution limit + const maxContractExecutionEnergy = Energy.create(dryRunResult.usedEnergy.value + EPSILON_ENERGY); const payload: Omit = { amount: CcdAmount.zero(), - address: ContractAddress.create(Number(process.env.AUCTION_CONTRACT_INDEX), CONTRACT_SUB_INDEX), + address: contract.contractAddress, receiveName: ReceiveName.create(AuctionContract.contractName, EntrypointName.fromString('addItem')), maxContractExecutionEnergy, }; @@ -94,13 +95,11 @@ export async function addItem( } /** - * TODO: update comment - * View item state. - * @param connection - The wallet connection to use for sending the transaction - * @param accountAddress - The account address to send from - * @param mintParameter - The parameter for the mint function - * @throws If the contract could not be updated - * @returns A promise resolving with the corresponding {@linkcode TransactionHash.Type} + * This function views the item state in the auction contract. + * + * @param viewItemState - The parameter for the viewItemState function. + * @throws If the communicate with the Concordium node fails, the smart contract reverts, or parsing the returnValue fails. + * @returns A promise resolving with the corresponding {@linkcode AuctionContract.ReturnValueViewItemState} */ export async function viewItemState( viewItemState: AuctionContract.ViewItemStateParameter, diff --git a/sponsoredTransactionsAuction/frontend/src/cis2_token_contract.tsx b/sponsoredTransactionsAuction/frontend/src/cis2_token_contract.tsx index a19977a6..1c2e1401 100644 --- a/sponsoredTransactionsAuction/frontend/src/cis2_token_contract.tsx +++ b/sponsoredTransactionsAuction/frontend/src/cis2_token_contract.tsx @@ -1,4 +1,4 @@ -import * as Cis2MultiContract from '../generated/cis2_multi_cis2_multi'; // Code generated from a smart contract module. +import * as Cis2MultiContract from '../generated/cis2_multi_cis2_multi'; // Code generated from a smart contract module. The naming convention of the generated file is `moduleName_smartContractName`. import { AccountTransactionType, @@ -33,14 +33,15 @@ const contract = Cis2MultiContract.createUnchecked( ContractAddress.create(Number(process.env.CIS2_TOKEN_CONTRACT_INDEX), CONTRACT_SUB_INDEX), ); -// This function submits a transaction to mint/airdrop tokens to an account. +export const CIS2_TOKEN_CONTRACT = contract; + /** - * Mints new cis2 tokens to the account specified in the mintParameter. + * This function submits a transaction to mint/airdrop cis2_multi tokens to an account. * - * @param connection - The wallet connection to use for sending the transaction - * @param accountAddress - The account address to send from - * @param mintParameter - The parameter for the mint function - * @throws If the contract could not be updated + * @param connection - The wallet connection to use for sending the transaction. + * @param accountAddress - The account address to send from. + * @param mintParameter - The parameter for the mint function. + * @throws If simulating the contract update fails or the `mintParameter.owner` is not an account. * @returns A promise resolving with the corresponding {@linkcode TransactionHash.Type} */ export async function mint( @@ -62,11 +63,11 @@ export async function mint( ); } - const maxContractExecutionEnergy = Energy.create(dryRunResult.usedEnergy.value + EPSILON_ENERGY); // + EPSILON_ENERGY needs to be here, as there seems to be an issue with running out of energy 1 energy prior to reaching the execution limit + const maxContractExecutionEnergy = Energy.create(dryRunResult.usedEnergy.value + EPSILON_ENERGY); const payload: Omit = { amount: CcdAmount.zero(), - address: ContractAddress.create(Number(process.env.CIS2_TOKEN_CONTRACT_INDEX), CONTRACT_SUB_INDEX), + address: contract.contractAddress, receiveName: ReceiveName.create(Cis2MultiContract.contractName, EntrypointName.fromString('mint')), maxContractExecutionEnergy, }; @@ -105,13 +106,11 @@ export async function mint( } /** - * Mints new cis2 tokens to the account specified in the mintParameter. + * This function views the nonce (CIS3 standard) of an acccount in the cis2_multi contract. * - * @param connection - The wallet connection to use for sending the transaction - * @param accountAddress - The account address to send from - * @param mintParameter - The parameter for the mint function - * @throws If the contract could not be updated - * @returns A promise resolving with the corresponding {@linkcode TransactionHash.Type} + * @param nonceOfParameter - The parameter for the nonceOf function. + * @throws If the communicate with the Concordium node fails, the smart contract reverts, or parsing the returnValue fails. + * @returns A promise resolving with the corresponding {@linkcode Cis2MultiContract.ReturnValueNonceOf} */ export async function nonceOf( nonceOfParameter: Cis2MultiContract.NonceOfParameter, diff --git a/sponsoredTransactionsAuction/frontend/src/components/AddItemToAuction.tsx b/sponsoredTransactionsAuction/frontend/src/components/AddItemToAuction.tsx index f2eeaf78..b0512a3c 100644 --- a/sponsoredTransactionsAuction/frontend/src/components/AddItemToAuction.tsx +++ b/sponsoredTransactionsAuction/frontend/src/components/AddItemToAuction.tsx @@ -14,8 +14,8 @@ import { } from '@concordium/web-sdk'; import { AUCTION_END, AUCTION_START } from '../constants'; -import { addItem } from '../auction_contract'; +import { addItem } from '../auction_contract'; import * as AuctionContract from '../../generated/sponsored_tx_enabled_auction_sponsored_tx_enabled_auction'; // Code generated from a smart contract module. interface ConnectionProps { @@ -88,7 +88,7 @@ export default function AddItemToAuction(props: ConnectionProps) { setItemIndex(parsedEvent.content.item_index); } else { - setItemIndexError('Tansaction failed or event decoding failed.'); + setItemIndexError('Tansaction failed and event decoding failed.'); } }) .catch((e) => { diff --git a/sponsoredTransactionsAuction/frontend/src/components/App/App.tsx b/sponsoredTransactionsAuction/frontend/src/components/App/App.tsx index 36fb1640..389fec39 100644 --- a/sponsoredTransactionsAuction/frontend/src/components/App/App.tsx +++ b/sponsoredTransactionsAuction/frontend/src/components/App/App.tsx @@ -21,9 +21,9 @@ import Footer from '../Footer'; import { BROWSER_WALLET, REFRESH_INTERVAL } from '../../constants'; import { nonceOf } from '../../cis2_token_contract'; -import * as Cis2MultiContract from '../../../generated/cis2_multi_cis2_multi'; // Code generated from a smart contract module. +import * as Cis2MultiContract from '../../../generated/cis2_multi_cis2_multi'; -/* +/** * The main component that manages the wallet connection. * It imports and displays the four components `MintTokens`, `AddItemToAuction`, `ViewItem`, and `Bid`. */ @@ -47,6 +47,9 @@ export default function App(props: WalletConnectionProps) { const [txHash, setTxHash] = useState(undefined); const [transactionError, setTransactionError] = useState(undefined); + /** + * This function querries the nonce (CIS3 standard) of an acccount in the cis2_multi contract. + */ const refreshNonce = useCallback(() => { if (grpcClient && account) { const nonceOfParam: Cis2MultiContract.NonceOfParameter = [AccountAddress.fromBase58(account)]; @@ -72,9 +75,10 @@ export default function App(props: WalletConnectionProps) { return () => clearInterval(interval); }, [refreshNonce]); - /* - * This function gets the public key of an account. - * This function works with a wallet account that has just one public-private key pair in its two-level key map. + /** + * This function gets the public key of an account. The function can be used + * for accounts generated in the browser and mobile wallets + * that have just one public-private key pair in their two-level key map. */ const getPublicKey = useCallback( (account: string | undefined) => { diff --git a/sponsoredTransactionsAuction/frontend/src/components/Bid.tsx b/sponsoredTransactionsAuction/frontend/src/components/Bid.tsx index 05db91b7..29ea3bd0 100644 --- a/sponsoredTransactionsAuction/frontend/src/components/Bid.tsx +++ b/sponsoredTransactionsAuction/frontend/src/components/Bid.tsx @@ -11,15 +11,14 @@ import { SERIALIZATION_HELPER_SCHEMA_ADDITIONAL_DATA, SERIALIZATION_HELPER_SCHEMA_PERMIT_MESSAGE, TRANSFER_SCHEMA, - VERIFIER_URL, } from '../constants'; - import { submitBid, validateAccountAddress } from '../utils'; -import { viewItemState } from '../auction_contract'; +import { AUCTION_CONTRACT, viewItemState } from '../auction_contract'; +import { CIS2_TOKEN_CONTRACT } from '../cis2_token_contract'; import * as AuctionContract from '../../generated/sponsored_tx_enabled_auction_sponsored_tx_enabled_auction'; // Code generated from a smart contract module. -/* +/** * This function generates the transfer message to be signed in the browser wallet. */ async function generateTransferMessage( @@ -37,6 +36,7 @@ async function generateTransferMessage( throw new Error(`ItemIndex is NaN.`); } + // Figure out the `tokenId` that the item is up for auction. const itemState: AuctionContract.ReturnValueViewItemState = await viewItemState(viewItemStateParam); setTokenID(itemState.token_id); @@ -48,21 +48,18 @@ async function generateTransferMessage( const hexStringData = [...data.buffer].map((b) => b.toString(16).padStart(2, '0')).join(''); + // Generate transfer parameter. const transfer = [ { amount, - data: hexStringData, // e.g. 0100 (for item with index 1) + // The item index in the auction contract is of type u16. + // A little endian hex string of 2 bytes represents the index here. E.g. `0100` for item with index 1. + data: hexStringData, from: { Account: [account], }, to: { - Contract: [ - { - index: Number(process.env.AUCTION_CONTRACT_INDEX), - subindex: 0, - }, - 'bid', - ], + Contract: [AUCTION_CONTRACT.contractAddress, 'bid'], }, token_id: itemState.token_id, }, @@ -71,10 +68,7 @@ async function generateTransferMessage( const payload = serializeTypeValue(transfer, toBuffer(TRANSFER_SCHEMA, 'base64')); const message = { - contract_address: { - index: Number(process.env.CIS2_TOKEN_CONTRACT_INDEX), - subindex: 0, - }, + contract_address: CIS2_TOKEN_CONTRACT.contractAddress, nonce: Number(nonce), timestamp: expiryTimeSignature, entry_point: 'transfer', @@ -99,7 +93,7 @@ interface ConnectionProps { setTransactionError: (error: string | undefined) => void; } -/* +/** * A component that manages the input fields and corresponding state to sign a bid message and submit the signature to the backend. */ export default function Bid(props: ConnectionProps) { @@ -129,6 +123,9 @@ export default function Bid(props: ConnectionProps) { } const formBid = useForm({ mode: 'all' }); + /** + * When submitting the form, the browser wallet is prompt to sign the transferMessage. + */ async function onSubmitSigning(data: FormTypeGenerateSignature) { setSigningError(undefined); setSignature(undefined); @@ -165,6 +162,9 @@ export default function Bid(props: ConnectionProps) { } } + /** + * When submitting the form, the generated signature from the previous step is sent to the backend. + */ function onSubmitBid(data: FormTypeBid, accountAddress: string | undefined) { setTxHash(undefined); setTransactionError(undefined); @@ -172,7 +172,6 @@ export default function Bid(props: ConnectionProps) { if (accountAddress && signature) { const tx = submitBid( - VERIFIER_URL, data.signer, nonce, signature, diff --git a/sponsoredTransactionsAuction/frontend/src/components/CCDScanLinks.tsx b/sponsoredTransactionsAuction/frontend/src/components/CCDScanLinks.tsx index c8e0daab..e8147c27 100644 --- a/sponsoredTransactionsAuction/frontend/src/components/CCDScanLinks.tsx +++ b/sponsoredTransactionsAuction/frontend/src/components/CCDScanLinks.tsx @@ -4,7 +4,7 @@ interface TxHashLinkProps { txHash: TransactionHash.Type; } -/* +/** * A component that displays the CCDScan link of a transaction hash. */ export const TxHashLink = function TxHashLink(props: TxHashLinkProps) { @@ -30,7 +30,7 @@ interface AccountLinkProps { account: string; } -/* +/** * A component that displays the CCDScan link to an account address. */ export const AccountLink = function AccountLink(props: AccountLinkProps) { diff --git a/sponsoredTransactionsAuction/frontend/src/components/Footer.tsx b/sponsoredTransactionsAuction/frontend/src/components/Footer.tsx index 11866a15..da152ab9 100644 --- a/sponsoredTransactionsAuction/frontend/src/components/Footer.tsx +++ b/sponsoredTransactionsAuction/frontend/src/components/Footer.tsx @@ -1,6 +1,6 @@ import { version } from '../../package.json'; -/* +/** * A component that displays a link to the developer documentation and the version of the app. */ export default function Footer() { diff --git a/sponsoredTransactionsAuction/frontend/src/components/MintTokens.tsx b/sponsoredTransactionsAuction/frontend/src/components/MintTokens.tsx index 135e9dd3..ab2b92b2 100644 --- a/sponsoredTransactionsAuction/frontend/src/components/MintTokens.tsx +++ b/sponsoredTransactionsAuction/frontend/src/components/MintTokens.tsx @@ -3,13 +3,13 @@ import { useForm } from 'react-hook-form'; import { Alert, Button, Form } from 'react-bootstrap'; import { WalletConnection } from '@concordium/react-components'; -import { mint } from '../cis2_token_contract'; import { AccountAddress, TransactionHash } from '@concordium/web-sdk'; import { METADATA_URL } from '../constants'; +import { validateAccountAddress } from '../utils'; +import { mint } from '../cis2_token_contract'; import * as Cis2MultiContract from '../../generated/cis2_multi_cis2_multi'; // Code generated from a smart contract module. -import { validateAccountAddress } from '../utils'; interface ConnectionProps { setTxHash: (hash: TransactionHash.Type | undefined) => void; @@ -18,7 +18,7 @@ interface ConnectionProps { connection: WalletConnection; } -/* +/** * A component that manages the input fields and corresponding state to mint/airdrop some cis2 tokens to the user. * This component creates an `Update` transaction. */ @@ -48,6 +48,9 @@ export default function MintTokens(props: ConnectionProps) { hash: { type: 'None' }, url: METADATA_URL, // In production, you should consider using a different metadata file for each token_id. }, + // The token id in the cis2 token contract is of type u8. + // A hex string of 1 byte represents the token id here. E.g. `01` for token id 1. + // The input field has a max/min value validation, so that `data.tokenID` is not greater than 255. token_id: `0${Number(data.tokenID).toString(16)}`.slice(-2), }; diff --git a/sponsoredTransactionsAuction/frontend/src/components/ViewItem.tsx b/sponsoredTransactionsAuction/frontend/src/components/ViewItem.tsx index 03818270..f5932157 100644 --- a/sponsoredTransactionsAuction/frontend/src/components/ViewItem.tsx +++ b/sponsoredTransactionsAuction/frontend/src/components/ViewItem.tsx @@ -13,7 +13,7 @@ interface ConnectionProps { grpcClient: ConcordiumGRPCClient | undefined; } -/* +/** * A component that manages the input fields and corresponding state to view an item in the auction contract. */ export default function ViewItem(props: ConnectionProps) { diff --git a/sponsoredTransactionsAuction/frontend/src/constants.ts b/sponsoredTransactionsAuction/frontend/src/constants.ts index a2a1b6c2..b9922fdf 100644 --- a/sponsoredTransactionsAuction/frontend/src/constants.ts +++ b/sponsoredTransactionsAuction/frontend/src/constants.ts @@ -16,6 +16,11 @@ export const AUCTION_CONTRACT_NAME = ContractName.fromString('sponsored_tx_enabl export const CONTRACT_SUB_INDEX = 0n; +// Before submitting a transaction we simulate/dry-run the transaction to get an +// estimate of the energy needed for executing the transaction. In addition, we +// allow an additional small amount of energy `EPSILON_ENERGY` to be consumed by +// the transaction to cover small variations (e.g. changes to the smart contract +// state) caused by transactions that have been executed meanwhile. export const EPSILON_ENERGY = 1000n; export const AUCTION_START = '2000-01-01T12:00:00Z'; // Hardcoded value for simplicity for this demo dApp. diff --git a/sponsoredTransactionsAuction/frontend/src/scss/_components.scss b/sponsoredTransactionsAuction/frontend/src/scss/_components.scss deleted file mode 100644 index abeeb5aa..00000000 --- a/sponsoredTransactionsAuction/frontend/src/scss/_components.scss +++ /dev/null @@ -1,2 +0,0 @@ -// Project components -// @import 'components/App'; diff --git a/sponsoredTransactionsAuction/frontend/src/scss/index.scss b/sponsoredTransactionsAuction/frontend/src/scss/index.scss index 8c4e8ef9..87071ece 100644 --- a/sponsoredTransactionsAuction/frontend/src/scss/index.scss +++ b/sponsoredTransactionsAuction/frontend/src/scss/index.scss @@ -1,4 +1,3 @@ @import 'config'; @import 'bootstrap-config'; @import 'layout'; -@import 'components'; diff --git a/sponsoredTransactionsAuction/frontend/src/scss/layout/_accordion.scss b/sponsoredTransactionsAuction/frontend/src/scss/layout/_accordion.scss deleted file mode 100644 index 367d6b04..00000000 --- a/sponsoredTransactionsAuction/frontend/src/scss/layout/_accordion.scss +++ /dev/null @@ -1,8 +0,0 @@ -@import 'bootstrap/scss/accordion'; - -.accordion-button { - &:not(.collapsed) { - color: $ocean-blue; - background: $header-gradient; - } -} diff --git a/sponsoredTransactionsAuction/frontend/src/scss/layout/_modal.scss b/sponsoredTransactionsAuction/frontend/src/scss/layout/_modal.scss deleted file mode 100644 index ad29c899..00000000 --- a/sponsoredTransactionsAuction/frontend/src/scss/layout/_modal.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import 'bootstrap/scss/modal'; - -.modal-header { - color: $ocean-blue; - background: $header-gradient; -} diff --git a/sponsoredTransactionsAuction/frontend/src/utils.ts b/sponsoredTransactionsAuction/frontend/src/utils.ts index 6d333018..78563e5f 100644 --- a/sponsoredTransactionsAuction/frontend/src/utils.ts +++ b/sponsoredTransactionsAuction/frontend/src/utils.ts @@ -1,11 +1,25 @@ import { AccountAddress, TransactionHash } from '@concordium/web-sdk'; -/* +import { + VERIFIER_URL, +} from './constants'; + +/** * This function sends the bidding signature and other parameters to the back end. * The back end will send the sponsored transaction on behalf of the user to the chain. + * + * @param signer - The account that signed the transferMessage. + * @param nonce - The nonce (CIS3 standard) that was part of the message that was signed. + * @param signature - The signature that was generated by the wallet. + * @param expiryTimeSignature - The timestamp when the signature expires. + * @param tokenID - The tokenID that was part of the message that was signed. + * @param from - The from address that was part of the message that was signed. + * @param tokenAmount - The tokenAmount that was part of the message that was signed. + * @param itemIndexAuction - The itemIndexAuction that was part of the message that was signed. + * @throws If the server responds with an error or the response of the server is malformed. + * @returns The {@linkcode TransactionHash.Type} of the transaction submitted at the backend. */ export async function submitBid( - backend: string, signer: string, nonce: string, signature: string, @@ -15,7 +29,7 @@ export async function submitBid( tokenAmount: string | undefined, itemIndexAuction: string | undefined, ) { - const response = await fetch(`${backend}/bid`, { + const response = await fetch(`${VERIFIER_URL}/bid`, { method: 'POST', headers: new Headers({ 'content-type': 'application/json' }), body: JSON.stringify({ @@ -41,12 +55,18 @@ export async function submitBid( throw new Error('Unable to submit bid'); } +/** + * This function validates if a string represents a valid accountAddress in base58 encoding. + * The length, characters, and the checksum are validated. + * + * @param accountAddress - An account address represented as a base58 encoded string. + * @returns An error message if validation fails. + */ export function validateAccountAddress(accountAddress: string) { try { AccountAddress.fromBase58(accountAddress); } catch (e) { - return `Please enter a valid account address. It is a base58 string with a fixed length of 50 characters. Original error: ${ - (e as Error).message - }.`; + return `Please enter a valid account address. It is a base58 string with a fixed length of 50 characters. Original error: ${(e as Error).message + }.`; } }