From c83da00bd0baae12090406721d77925ce5e41859 Mon Sep 17 00:00:00 2001 From: Jan Marcano Date: Fri, 30 Sep 2022 13:46:34 -0300 Subject: [PATCH 1/6] Update validations from ARC-0001 w/ new Errors --- packages/common/src/errors.ts | 91 ++++++--- packages/common/src/types.ts | 1 + .../src/background/messaging/handler.ts | 6 +- .../background/messaging/internalMethods.ts | 6 +- .../src/background/messaging/task.ts | 187 +++++++++++++----- 5 files changed, 215 insertions(+), 76 deletions(-) diff --git a/packages/common/src/errors.ts b/packages/common/src/errors.ts index 3afe8b07..85640897 100644 --- a/packages/common/src/errors.ts +++ b/packages/common/src/errors.ts @@ -9,57 +9,84 @@ export class RequestError { static None = new RequestError('', 0); static Undefined = new RequestError( - '[RequestError.Undefined] An undefined error occurred.', + 'An undefined error occurred.', 4000 ); static UserRejected = new RequestError( - '[RequestError.UserRejected] The extension user does not authorize the request.', + 'The extension user does not authorize the request.', 4001 ); - static NotAuthorizedByUser = new RequestError( - '[RequestError.NotAuthorized] The extension user does not authorize the request.', + static SiteNotAuthorizedByUser = new RequestError( + 'The extension user has not authorized requests from this website.', + 4100 + ); + static NoMnemonicAvailable = (address: string): RequestError => new RequestError( + `The user does not possess the required private key to sign with for address: "${address}".`, 4100 ); static NoAccountMatch = (address: string, ledger: string): RequestError => new RequestError( - `No matching account found on AlgoSigner for address: "${address}" on network ${ledger}.`, + `No matching account found on AlgoSigner for address "${address}" on network ${ledger}.`, 4100 ); - static UnsupportedAlgod = new RequestError( - '[RequestError.UnsupportedAlgod] The provided method is not supported.', - 4200 - ); static UnsupportedLedger = new RequestError( - '[RequestError.UnsupportedLedger] The provided ledger is not supported.', - 4200 - ); - static NotAuthorizedOnChain = new RequestError( - 'The user does not possess the required private key to sign with this address.', - 4200 - ); - static MultipleTxsRequireGroup = new RequestError( - 'If signing multiple transactions, they need to belong to a same group.', + 'The provided ledger is not supported.', 4200 ); static PendingTransaction = new RequestError('Another query processing', 4201); static LedgerMultipleTransactions = new RequestError( - 'Ledger hardware device signing not available for multiple transactions.', + 'Ledger hardware device signing is only available for one transaction at a time.', 4201 ); static TooManyTransactions = new RequestError( - `The ledger does not support signing more than ${RequestError.MAX_GROUP_SIZE} transactions at a time.`, + `AlgoSigner does not support signing more than ${RequestError.MAX_GROUP_SIZE} transactions at a time.`, 4201 ); - static InvalidFields = (data?: any) => + static AlgoSignerNotInitialized = new RequestError( + 'AlgoSigner was not initialized properly beforehand.', + 4202 + ); + static InvalidFields = (data?: any): RequestError => new RequestError('Validation failed for transaction due to invalid properties.', 4300, data); - static InvalidTransactionStructure = (data?: any) => + static InvalidTransactionStructure = (data?: any): RequestError => new RequestError('Validation failed for transaction due to invalid structure.', 4300, data); static InvalidFormat = new RequestError( '[RequestError.InvalidFormat] Please provide an array of either valid transaction objects or nested arrays of valid transaction objects.', 4300 ); - static InvalidSigners = new RequestError( - 'Signers array should only be provided for multisigs (at least one signer) or for reference-only transactions belonging to a group (empty array).', + static CantMatchMsigSigners = (info: string): RequestError => + new RequestError( + `AlgoSigner does not currently possess one of the requested signers for this multisig transaction: ${info}.`, + 4300, + ); + static InvalidSignerAddress = (address: string): RequestError => + new RequestError(`Signers array contains the invalid address "${address}"`, 4300); + static MsigSignersMismatch = new RequestError( + "The 'signers' array contains addresses that aren't subaccounts of the requested multisig account.", + 4300 + ); + static NoMsigSingleSigner = new RequestError( + "When a single-address 'signers' is provided with no 'msig' alongside it, it must match either the transaction sender or (if provided) the 'authAddr'", + 4300 + ); + static NoMsigMultipleSigners = new RequestError( + "Multiple (1+) 'signers' should only be used alongside 'msig' transactions to specify which subaccounts to sign with.", + 4300 + ); + static InvalidSignedTxn = new RequestError( + "The signed transaction provided ('{ stxn:... }') could not be parsed.", + 4300 + ); + static NonMatchingSignedTxn = new RequestError( + "The signed transaction provided ('{ stxn:... }') doesn't match the corresponding unsigned transaction ('{ txn:... }').", + 4300 + ); + static SignedTxnWithSigners = new RequestError( + "A signed transaction ('{ stxn:... }') must only be provided for reference transactions ('{ signers: [] }').", + 4300 + ); + static NoTxsToSign = new RequestError( + "There are no transactions to sign as the provided ones are for reference-only ('{ signers: [] }').", 4300 ); static InvalidStructure = new RequestError( @@ -70,10 +97,24 @@ export class RequestError { "The provided multisig data doesn't adhere to the correct structure.", 4300 ); + static InvalidMsigValues = (reason: string): RequestError => + new RequestError(`The provided multisig data has invalid values: ${reason}.`, 4300); + static InvalidMsigAddress = (address: string): RequestError => + new RequestError(`The address '${address}' is invalid.`, 4300); + static MsigAuthAddrMismatch = new RequestError( + "The provided 'authAddr' doesn't match the requested multisig account.", + 4300 + ); + static InvalidAuthAddress = (address: string): RequestError => + new RequestError(`'authAddr' contains the invalid address "${address}"`, 4300); static IncompleteOrDisorderedGroup = new RequestError( 'The transaction group is incomplete or presented in a different order than when it was created.', 4300 ); + static MultipleTxsRequireGroup = new RequestError( + 'If signing multiple transactions, they need to belong to a same group.', + 4300 + ); static NonMatchingGroup = new RequestError( 'All transactions need to belong to the same group.', 4300 @@ -82,7 +123,7 @@ export class RequestError { 'All transactions need to belong to the same ledger.', 4300 ); - static SigningError = (code: number, data?: any) => + static SigningError = (code: number, data?: any): RequestError => new RequestError('There was a problem signing the transaction(s).', code, data); protected constructor(message: string, code: number, data?: any) { diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index e58edf09..1179f892 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -26,6 +26,7 @@ export type WalletMultisigMetadata = { export type WalletTransaction = { readonly txn: string; readonly signers?: Array; + readonly stxn?: string; readonly message?: string; readonly msig?: WalletMultisigMetadata; readonly authAddr?: string; diff --git a/packages/extension/src/background/messaging/handler.ts b/packages/extension/src/background/messaging/handler.ts index fa55ca60..0a150e0c 100644 --- a/packages/extension/src/background/messaging/handler.ts +++ b/packages/extension/src/background/messaging/handler.ts @@ -41,7 +41,7 @@ export class OnMessageHandler extends RequestValidation { try { request.origin = new URL(sender.url).origin; } catch (e) { - request.error = RequestError.NotAuthorizedByUser; + request.error = RequestError.SiteNotAuthorizedByUser; MessageApi.send(request); return; } @@ -79,7 +79,7 @@ export class OnMessageHandler extends RequestValidation { new encryptionWrap('').checkStorage((exist: boolean) => { // Reject message if there's no wallet if (!exist) { - request.error = RequestError.NotAuthorizedByUser; + request.error = RequestError.AlgoSignerNotInitialized; MessageApi.send(request); } else { if (OnMessageHandler.isAuthorization(method) && OnMessageHandler.isPublic(method)) { @@ -98,7 +98,7 @@ export class OnMessageHandler extends RequestValidation { }); } else { // Origin is not authorized - request.error = RequestError.NotAuthorizedByUser; + request.error = RequestError.SiteNotAuthorizedByUser; MessageApi.send(request); } } diff --git a/packages/extension/src/background/messaging/internalMethods.ts b/packages/extension/src/background/messaging/internalMethods.ts index 7c67f627..7cbe18f3 100644 --- a/packages/extension/src/background/messaging/internalMethods.ts +++ b/packages/extension/src/background/messaging/internalMethods.ts @@ -97,8 +97,8 @@ export class InternalMethods { }); } - // Checks if an address is a valid user account for a given ledger. - public static checkValidAccount(genesisID: string, address: string): void { + // Checks if an account for the given address exists on AlgoSigner for a given ledger. + public static checkAccountIsImported(genesisID: string, address: string): void { const ledger: string = getLedgerFromGenesisId(genesisID); let found = false; for (let i = session.wallet[ledger].length - 1; i >= 0; i--) { @@ -891,7 +891,7 @@ export class InternalMethods { // Return to close connection return true; } else if (!account.mnemonic) { - sendResponse({ error: RequestError.NotAuthorizedOnChain.message }); + sendResponse({ error: RequestError.NoMnemonicAvailable(account.address).message }); } else { // We can use a modified popup to allow the normal flow, but require extra scrutiny. const recoveredAccount = algosdk.mnemonicToSecretKey(account.mnemonic); diff --git a/packages/extension/src/background/messaging/task.ts b/packages/extension/src/background/messaging/task.ts index 81253414..c201629d 100644 --- a/packages/extension/src/background/messaging/task.ts +++ b/packages/extension/src/background/messaging/task.ts @@ -1,4 +1,4 @@ -import algosdk from 'algosdk'; +import algosdk, { MultisigMetadata, Transaction } from 'algosdk'; import { WalletTransaction } from '@algosigner/common/types'; import { RequestError } from '@algosigner/common/errors'; @@ -197,7 +197,7 @@ export class Task { const groupsToSign: Array> = request.body.params.groupsToSign; const currentGroup: number = request.body.params.currentGroup; const walletTransactions: Array = groupsToSign[currentGroup]; - const rawTxArray: Array = []; + const rawTxArray: Array = []; const processedTxArray: Array = []; const transactionWraps: Array = []; const validationErrors: Array = []; @@ -214,11 +214,12 @@ export class Task { if ( // prettier-ignore (walletTx.authAddr != null && typeof walletTx.authAddr !== 'string') || + (walletTx.stxn != null && typeof walletTx.stxn !== 'string') || (walletTx.message != null && typeof walletTx.message !== 'string') || (!walletTx.txn || typeof walletTx.txn !== 'string') || - (walletTx.signers != null && + (walletTx.signers != null && ( - !Array.isArray(walletTx.signers) || + !Array.isArray(walletTx.signers) || (Array.isArray(walletTx.signers) && (walletTx.signers as Array).some((s)=>typeof s !== 'string')) ) ) || @@ -232,8 +233,8 @@ export class Task { (!walletTx.msig.threshold || typeof walletTx.msig.threshold !== 'number') || (!walletTx.msig.version || typeof walletTx.msig.version !== 'number') || ( - !walletTx.msig.addrs || - !Array.isArray(walletTx.msig.addrs) || + !walletTx.msig.addrs || + !Array.isArray(walletTx.msig.addrs) || (Array.isArray(walletTx.msig.addrs) && (walletTx.msig.addrs as Array).some((s)=>typeof s !== 'string')) ) ) @@ -249,7 +250,9 @@ export class Task { * 2) Use the '_getDictForDisplay' to change the format of the fields that are different from ours * 3) Remove empty fields to get rid of conversion issues like empty note byte arrays */ - const rawTx = algosdk.decodeUnsignedTransaction(base64ToByteArray(walletTx.txn)); + const rawTx: Transaction = algosdk.decodeUnsignedTransaction( + base64ToByteArray(walletTx.txn) + ); rawTxArray[index] = rawTx; const processedTx = rawTx._getDictForDisplay(); processedTxArray[index] = processedTx; @@ -257,25 +260,111 @@ export class Task { transactionWraps[index] = wrap; const genesisID = wrap.transaction.genesisID; - const signers = walletTransactions[index].signers; - const msigData = walletTransactions[index].msig; - const authAddr = walletTransactions[index].authAddr; + const signers: Array = walletTransactions[index].signers; + const signedTxn: string = walletTransactions[index].stxn; + const msigData: MultisigMetadata = walletTransactions[index].msig; + const authAddr: string = walletTransactions[index].authAddr; - wrap.msigData = msigData; + let msigAddress: string; wrap.signers = signers; + + // We validate the authAddress if available + if (authAddr && !algosdk.isValidAddress(authAddr)) { + throw RequestError.InvalidAuthAddress(authAddr); + } + + // If we have msigData, we validate the addresses and fetch the resulting msig address if (msigData) { - if (signers && signers.length) { + try { + wrap.msigData = msigData; + msigAddress = algosdk.multisigAddress(msigData); + if (authAddr && authAddr !== msigAddress) { + throw RequestError.MsigAuthAddrMismatch; + } + msigData.addrs.forEach((addr) => { + if (!algosdk.isValidAddress(addr)) { + throw RequestError.InvalidMsigAddress(addr); + } + }); + } catch (e) { + throw RequestError.InvalidMsigValues(e.message); + } + } + + // We check if signers were specificied for this txn + if (signers) { + if (signers.length) { + // We have some specific signers, so we must validate them + // First we check if there's a stxn provided + if (signedTxn) { + throw RequestError.SignedTxnWithSigners; + } + // Then, we check all signers are valid accounts existing in AlgoSigner signers.forEach((address) => { - InternalMethods.checkValidAccount(genesisID, address); + if (!algosdk.isValidAddress(address)) { + throw RequestError.InvalidSignerAddress(address); + } }); + + // We validate signers depending on the amount of them + if (signers.length > 1) { + // We have more than 1 signer, they're for 'msig' use + if (!msigData) { + // If no msig info was found, we reject + throw RequestError.NoMsigMultipleSigners; + } + } else { + // We have exactly 1 signer. If there's no 'msig', additional validations + if (!msigData) { + // If we have an authAddr the signer must match it + if (authAddr && authAddr !== signers[0]) { + throw RequestError.NoMsigSingleSigner; + } + // Otherwise, it should at least match the sender + if (!authAddr && signers[0] !== processedTx.from) { + throw RequestError.NoMsigSingleSigner; + } + } + } + if (msigData) { + // Msig was provided alongside signers, we validate joint use cases + // Signers must be a subset of the multisig addresses + if (!signers.every((s) => msigData.addrs.includes(s))) { + throw RequestError.MsigSignersMismatch; + } + // We make sure we have the available accounts for signing + signers.forEach((address) => { + try { + InternalMethods.checkAccountIsImported(genesisID, address); + } catch (e) { + throw RequestError.CantMatchMsigSigners(e.message); + } + }); + } + } else { + // Empty signer was provided, we check for a 'stxn' + if (signedTxn) { + // We check if the provided stxn matches the txn received + const unsignedTxnBytes = rawTx.toByte(); + let signedTxnBytes; + try { + signedTxnBytes = algosdk + .decodeSignedTransaction(base64ToByteArray(signedTxn)) + .txn.toByte(); + } catch (e) { + // We reject if we can't convert from b64 to a valid txn + throw RequestError.InvalidSignedTxn; + } + if (!signedTxnBytes.every((value, index) => unsignedTxnBytes[index] === value)) { + // We reject if the transactions don't match + throw RequestError.NonMatchingSignedTxn; + } + } } - wrap.msigData = msigData; } else { - if (!signers) { - InternalMethods.checkValidAccount(genesisID, wrap.transaction.from); - } else if (authAddr && signers.length === 1 && authAddr !== signers[0]) { - // We have an authAddr so if signers is length of 1 then they must be equal - throw RequestError.InvalidFormat; + // There's no signers field, we validate the sender if there's no msig + if (!msigData) { + InternalMethods.checkAccountIsImported(genesisID, wrap.transaction.from); } } @@ -283,12 +372,6 @@ export class Task { // Attach to wrap so it can be displayed transactionWraps[index].authAddr = authAddr; - // Check that the authAddr is an address - const isValidAddress = algosdk.isValidAddress(authAddr); - if (!isValidAddress) { - throw RequestError.UnsupportedAlgod; - } - // If there is an auth address then we SHOULD validate it is on chain and warn if not present const chainAuthAddr = await Task.getChainAuthAddress(processedTx); // If there was an auth address on chain then set the auth address for the transaction @@ -316,9 +399,12 @@ export class Task { let data = ''; let code = 4300; + // We format the validation errors for better readability and clarity validationErrors.forEach((error, index) => { + // Concatenate the errors in a single formatted message data = data + `Validation failed for transaction ${index} due to: ${error.message}. `; - code = error.code && error.code < code ? error.code : code; + // Take the lowest error code as they're _more_ generic the higher they go + code = error.code < code ? error.code : code; }); throw RequestError.SigningError(code, data.trim()); } else if ( @@ -354,7 +440,9 @@ export class Task { throw RequestError.InvalidFields(data); } else { - // Group validations + /** + * Group validations + */ const groupId = transactionWraps[0].transaction.group; if (transactionWraps.length > 1) { @@ -373,14 +461,6 @@ export class Task { if (!transactionWraps.every((wrap) => groupId === wrap.transaction.group)) { throw RequestError.NonMatchingGroup; } - } else { - const wrap = transactionWraps[0]; - if ( - (!wrap.msigData && wrap.signers) || - (wrap.msigData && wrap.signers && !wrap.signers.length) - ) { - throw RequestError.InvalidSigners; - } } if (groupId) { @@ -397,6 +477,11 @@ export class Task { } } + // If we only receive reference transactions, we reject + if (!transactionWraps.some((wrap) => !wrap.signers || (wrap.signers && wrap.signers.length))) { + throw RequestError.NoTxsToSign; + } + for (let i = 0; i < transactionWraps.length; i++) { const wrap = transactionWraps[i]; await Task.modifyTransactionWrapWithAssetCoreInfo(wrap); @@ -920,7 +1005,7 @@ export class Task { if (!account.isHardware) { // Check for an address that we were expected but unable to sign with if (!unlockedValue[ledger][i].mnemonic) { - throw RequestError.NotAuthorizedByUser; + throw RequestError.NoMnemonicAvailable(account.address); } recoveredAccounts[account.address] = algosdk.mnemonicToSecretKey( unlockedValue[ledger][i].mnemonic @@ -931,18 +1016,30 @@ export class Task { } } - transactionObjs.forEach((tx, index) => { - const signers = walletTransactions[index].signers; + transactionObjs.forEach((txn: Transaction, index) => { + const signers: Array = walletTransactions[index].signers; + const signedTxn = walletTransactions[index].stxn; const authAddr = walletTransactions[index].authAddr; + const msigData: MultisigMetadata = walletTransactions[index].msig; + const txID = txn.txID().toString(); + const wrap = transactionsWraps[index]; - // If it's a reference transaction we return null, otherwise we try sign + // Check if it's a reference transaction if (signers && !signers.length) { - signedTxs[index] = null; + // It's a reference transaction, return the 'stxn' + if (signedTxn) { + // We return the provided stxn since it's a reference transaction + signedTxs[index] = { + txID: txID, + blob: signedTxn, + }; + } else { + // No signed transaction was provided, we return null + signedTxs[index] = null; + } } else { + // It's NOT a reference transaction, we sign normally try { - const txID = tx.txID().toString(); - const wrap = transactionsWraps[index]; - const msigData = wrap.msigData; let signedBlob; if (msigData) { @@ -955,7 +1052,7 @@ export class Task { if (recoveredAccounts[address]) { partiallySignedBlobs.push( algosdk.signMultisigTransaction( - tx, + txn, msigData, recoveredAccounts[address].sk ).blob @@ -976,7 +1073,7 @@ export class Task { } else { const address = authAddr || wrap.transaction.from; if (recoveredAccounts[address]) { - signedBlob = tx.signTxn(recoveredAccounts[address].sk); + signedBlob = txn.signTxn(recoveredAccounts[address].sk); const b64Obj = byteArrayToBase64(signedBlob); signedTxs[index] = { From 28cf6f7268da35cf6a45021100c9ceb7d3084450 Mon Sep 17 00:00:00 2001 From: Jan Marcano Date: Fri, 30 Sep 2022 13:54:04 -0300 Subject: [PATCH 2/6] Update Errors documentation --- docs/dApp-integration.md | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/docs/dApp-integration.md b/docs/dApp-integration.md index 79d53fc1..a0de00dd 100644 --- a/docs/dApp-integration.md +++ b/docs/dApp-integration.md @@ -459,22 +459,36 @@ AlgoSigner.send({ - Custom networks beta support is now in AlgoSigner. [Setup Guide](add-network.md) - AlgoSigner.accounts(ledger) has changed such that calls now accept names that have been added to the user's custom network list as valid ledger names. - A non-matching ledger name will result in a error: - - [RequestError.UnsupportedLedger] The provided ledger is not supported. + - The provided ledger is not supported (Code: 4200). - An empty request will result with an error: - Ledger not provided. Please use a base ledger: [TestNet,MainNet] or an available custom one [{"name":"Theta","genesisId":"thetanet-v1.0"}]. - Transaction requests will require a valid matching "genesisId", even for custom networks. -## Rejection Messages +## Signature Rejection Messages -The dApp may return the following errors in case of users rejecting requests, or errors in the request: +AlgoSigner may return some of the following error codes when requesting signatures: + +| Error Code | Description | Additional notes | +| ----------- | ----------- | --------------- | +| 4000 | An unknown error occured. | N/A | +| 4001 | The user rejected the signature request. | N/A | +| 4100 | The requested operation and/or account has not been authorized by the user. | This is usually due to the connection between the dApp and the wallet becoming stale and the user [needing to reconnect](connection-issues.md). Otherwise, it may signal that you are trying to sign with private keys not found on AlgoSigner. | +| 4200 | The wallet does not support the requested operation. | N/A | +| 4201 | The wallet does not support signing that many transactions at a time. | The max number of transactions per group is 16. For Ledger devices, they can't sign more than one transaction at the same time. | +| 4202 | The wallet was not initialized properly beforehand. | Users need to have imported or created an account on AlgoSigner before connecting to dApps | +| 4300 | The input provided is invalid. | AlgoSigner rejected some of the transactions due to invalid fields. | + +Additional information, if available, would be provided in the `data` field of the error object. + +Returned errors have the following object structure: ``` - UserRejected = '[RequestError.UserRejected] The extension user does not authorize the request.', - NotAuthorized = '[RequestError.NotAuthorized] The extension user does not authorize the request.', - UnsupportedAlgod = '[RequestError.UnsupportedAlgod] The provided method is not supported.', - UnsupportedLedger = '[RequestError.UnsupportedLedger] The provided ledger is not supported.', - InvalidFormat = '[RequestError.InvalidFormat] Please provide an array of either valid transaction objects or nested arrays of valid transaction objects.', - Undefined = '[RequestError.Undefined] An undefined error occurred.', +{ + message: string; + code: number; + name: string; + data?: any; +} ``` Errors may be passed back to the dApp from the Algorand JS SDK if a transaction is valid, but has some other issue - for example, insufficient funds in the sending account. From fd8fd3935465f45efc224b4c3e7f34344cd6c65e Mon Sep 17 00:00:00 2001 From: Jan Marcano Date: Fri, 30 Sep 2022 13:54:41 -0300 Subject: [PATCH 3/6] Update & Add tests for the new errors --- packages/common/src/common.test.ts | 2 +- packages/test-project/tests/common/helpers.js | 45 +++ packages/test-project/tests/common/tests.js | 6 +- .../test-project/tests/dapp-groups.test.js | 126 +++++- .../test-project/tests/dapp-signtxn.test.js | 363 +++++++++--------- 5 files changed, 351 insertions(+), 191 deletions(-) diff --git a/packages/common/src/common.test.ts b/packages/common/src/common.test.ts index 93291e2c..83ee3526 100644 --- a/packages/common/src/common.test.ts +++ b/packages/common/src/common.test.ts @@ -40,7 +40,7 @@ test('RequestError - Structure', () => { const testError = RequestError.NoAccountMatch(address, ledger); expect(testError).toMatchObject({ - message: `No matching account found on AlgoSigner for address: "${address}" on network ${ledger}.`, + message: `No matching account found on AlgoSigner for address "${address}" on network ${ledger}.`, code: 4100, }); diff --git a/packages/test-project/tests/common/helpers.js b/packages/test-project/tests/common/helpers.js index 6f7eb5fc..dc132073 100644 --- a/packages/test-project/tests/common/helpers.js +++ b/packages/test-project/tests/common/helpers.js @@ -133,6 +133,50 @@ async function getLedgerSuggestedParams(ledger = 'TestNet') { }; } +async function signDappTxns(transactionsToSign, testFunction) { + const timestampedName = `popupTest-${new Date().getTime().toString()}`; + if (testFunction) { + await dappPage.exposeFunction(timestampedName, async () => { + try { + await testFunction(); + } catch (e) { + console.log(e); + } + }); + } + + await dappPage.waitForTimeout(2000); + const signedTransactions = await dappPage.evaluate( + async (transactionsToSign, testFunction, testTimestamp) => { + const signPromise = AlgoSigner.signTxn(transactionsToSign) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + + if (testFunction) { + await window[testTimestamp](); + } + + await window['authorizeSignTxn'](); + return await Promise.resolve(signPromise); + }, + transactionsToSign, + !!testFunction, + timestampedName + ); + for (let i = 0; i < signedTransactions.length; i++) { + const signedTx = signedTransactions[i]; + if (signedTx) { + await expect(signedTx).toHaveProperty('txID'); + await expect(signedTx).toHaveProperty('blob'); + } + } + return signedTransactions; +} + async function sendTransaction(blob) { const sendBody = { ledger: 'TestNet', @@ -221,6 +265,7 @@ module.exports = { getOpenedTab, getPopup, getLedgerSuggestedParams, + signDappTxns, sendTransaction, base64ToByteArray, byteArrayToBase64, diff --git a/packages/test-project/tests/common/tests.js b/packages/test-project/tests/common/tests.js index 5db91116..5df79faf 100644 --- a/packages/test-project/tests/common/tests.js +++ b/packages/test-project/tests/common/tests.js @@ -169,7 +169,7 @@ function ConnectAlgoSigner() { await dappPage.exposeFunction('authorizeSignTxnGroups', authorizeSignTxnGroups); }); - test('NotAuthorized error before connecting', async () => { + test('SiteNotAuthorizedByUser error before connecting', async () => { await expect( dappPage.evaluate(() => { return Promise.resolve(AlgoSigner.accounts()) @@ -181,7 +181,7 @@ function ConnectAlgoSigner() { }); }) ).resolves.toMatchObject({ - message: expect.stringContaining('[RequestError.NotAuthorized]'), + message: expect.stringContaining('The extension user has not'), code: 4100, }); }); @@ -200,7 +200,7 @@ function ConnectAlgoSigner() { }); }) ).resolves.toMatchObject({ - message: expect.stringContaining('[RequestError.UserRejected]'), + message: expect.stringContaining('The extension user does not'), code: 4001, }); }); diff --git a/packages/test-project/tests/dapp-groups.test.js b/packages/test-project/tests/dapp-groups.test.js index e62a6270..e720103a 100644 --- a/packages/test-project/tests/dapp-groups.test.js +++ b/packages/test-project/tests/dapp-groups.test.js @@ -9,6 +9,7 @@ const { accounts } = require('./common/constants'); const { openExtension, getLedgerSuggestedParams, + signDappTxns, buildSdkTx, prepareWalletTx, } = require('./common/helpers'); @@ -17,8 +18,9 @@ const { CreateWallet, ConnectAlgoSigner, ImportAccount } = require('./common/tes const account = accounts.ui; let ledgerParams; +let tx1, tx2, tx3, tx4; -async function signTxnGroups(transactionsToSign) { +async function signDappTxnGroups(transactionsToSign) { await dappPage.waitForTimeout(2000); const signedGroups = await dappPage.evaluate( async (transactionsToSign) => { @@ -65,9 +67,125 @@ describe('Wallet Setup', () => { ImportAccount(account); }); -describe('Group of Groups Use cases', () => { - let tx1, tx2, tx3, tx4; +describe('Group Transactions Use cases', () => { + test('Reject on incomplete Group', async () => { + tx1 = buildSdkTx({ + type: 'pay', + from: account.address, + to: account.address, + amount: Math.ceil(Math.random() * 1000), + ...ledgerParams, + fee: 1000, + }); + const groupedTransactions = algosdk.assignGroupID([tx1, tx1]); + const unsignedTransactions = [prepareWalletTx(groupedTransactions[0])]; + + await expect( + dappPage.evaluate((transactions) => { + return Promise.resolve(AlgoSigner.signTxn(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions) + ).resolves.toMatchObject({ + message: expect.stringContaining('group is incomplete'), + code: 4300, + name: expect.stringContaining('AlgoSignerRequestError'), + }); + }); + + test('Accept Group ID for Single Transactions', async () => { + tx1 = buildSdkTx({ + type: 'pay', + from: account.address, + to: account.address, + amount: Math.ceil(Math.random() * 1000), + ...ledgerParams, + fee: 1000, + }); + const groupedTransactions = algosdk.assignGroupID([tx1]); + const unsignedTransactions = [prepareWalletTx(groupedTransactions[0])]; + + const signedTransactions = await signDappTxns(unsignedTransactions); + await expect(signedTransactions[0]).not.toBeNull(); + }); + + test('Group Transaction with Reference Transaction && Pooled Fee', async () => { + tx1 = buildSdkTx({ + type: 'pay', + from: account.address, + to: account.address, + amount: Math.ceil(Math.random() * 1000), + ...ledgerParams, + fee: 1000, + }); + tx2 = buildSdkTx({ + type: 'pay', + from: account.address, + to: account.address, + amount: Math.ceil(Math.random() * 1000), + ...ledgerParams, + }); + tx3 = buildSdkTx({ + type: 'pay', + from: account.address, + to: account.address, + amount: Math.ceil(Math.random() * 1000), + ...ledgerParams, + }); + + const groupedTransactions = await algosdk.assignGroupID([tx1, tx2, tx3]); + const unsignedTransactions = groupedTransactions.map((txn) => prepareWalletTx(txn)); + unsignedTransactions[2].signers = []; + + const signedTransactions = await signDappTxns(unsignedTransactions); + await expect(signedTransactions[2]).toBeNull(); + await expect(signedTransactions.filter((i) => i)).toHaveLength(2); + }); + + test('Max # of Transactions on Group', async () => { + const tx = buildSdkTx({ + type: 'pay', + from: account.address, + to: account.address, + amount: Math.ceil(Math.random() * 1000), + ...ledgerParams, + fee: 1000, + }); + + const txArray = []; + for (let i = 0; i < 16; i++) { + txArray.push(tx); + } + + const groupedTransactions = await algosdk.assignGroupID(txArray); + groupedTransactions[16] = groupedTransactions[0]; + const unsignedTransactions = groupedTransactions.map((txn) => prepareWalletTx(txn)); + + await expect( + dappPage.evaluate((transactions) => { + return Promise.resolve(AlgoSigner.signTxn(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions) + ).resolves.toMatchObject({ + message: expect.stringContaining('16 transactions at a time'), + code: 4201, + name: expect.stringContaining('AlgoSignerRequestError'), + }); + }); + + // @TODO: Add errors for mismatches, incomplete groups, etc +}); +describe('Group of Groups Use cases', () => { test('Group of Grouped Transactions', async () => { tx1 = buildSdkTx({ type: 'pay', @@ -105,7 +223,7 @@ describe('Group of Groups Use cases', () => { group1[1].signers = []; const group2 = await algosdk.assignGroupID([tx3, tx4]).map((txn) => prepareWalletTx(txn)); - const signedTransactions = await signTxnGroups([group1, group2]); + const signedTransactions = await signDappTxnGroups([group1, group2]); await expect(signedTransactions).toHaveLength(2); await expect(signedTransactions[0][1]).toBeNull(); }); diff --git a/packages/test-project/tests/dapp-signtxn.test.js b/packages/test-project/tests/dapp-signtxn.test.js index 4b9c5199..e2e7ad37 100644 --- a/packages/test-project/tests/dapp-signtxn.test.js +++ b/packages/test-project/tests/dapp-signtxn.test.js @@ -4,12 +4,12 @@ * @group dapp/signtxn */ -const algosdk = require('algosdk'); const { accounts } = require('./common/constants'); const { openExtension, getPopup, getLedgerSuggestedParams, + signDappTxns, decodeBase64Blob, buildSdkTx, prepareWalletTx, @@ -21,51 +21,8 @@ const account1 = msigAccount.subaccounts[0]; const account2 = msigAccount.subaccounts[1]; let ledgerParams; -let unsignedTransactions = []; - -async function signTxn(transactionsToSign, testFunction) { - const timestampedName = `popupTest-${new Date().getTime().toString()}`; - if (testFunction) { - await dappPage.exposeFunction(timestampedName, async () => { - try { - await testFunction(); - } catch (e) { - console.log(e); - } - }); - } - - await dappPage.waitForTimeout(2000); - const signedTransactions = await dappPage.evaluate( - async (transactionsToSign, testFunction, testTimestamp) => { - const signPromise = AlgoSigner.signTxn(transactionsToSign) - .then((data) => { - return data; - }) - .catch((error) => { - return error; - }); - - if (testFunction) { - await window[testTimestamp](); - } - - await window['authorizeSignTxn'](); - return await Promise.resolve(signPromise); - }, - transactionsToSign, - !!testFunction, - timestampedName - ); - for (let i = 0; i < signedTransactions.length; i++) { - const signedTx = signedTransactions[i]; - if (signedTx) { - await expect(signedTx).toHaveProperty('txID'); - await expect(signedTx).toHaveProperty('blob'); - } - } - return signedTransactions; -} +let unsignedTransactions; +let msigTxn; describe('Wallet Setup', () => { beforeAll(async () => { @@ -77,14 +34,31 @@ describe('Wallet Setup', () => { test('Get TestNet params', async () => { ledgerParams = await getLedgerSuggestedParams(); + const baseTxn = prepareWalletTx( + buildSdkTx({ + type: 'pay', + from: msigAccount.address, + to: msigAccount.address, + amount: Math.ceil(Math.random() * 1000), + ...ledgerParams, + fee: 1000, + }) + ); + const msigMetadata = { + version: 1, + threshold: 2, + addrs: msigAccount.subaccounts.map((acc) => acc.address), + }; + msigTxn = { ...baseTxn, msig: msigMetadata }; }); ImportAccount(account1); ImportAccount(account2); }); -describe('Error Use cases', () => { - test('UserRejected error upon signing refusal', async () => { +describe('Txn Signing Validation errors', () => { + // General validations + test('Error on User signing refusal', async () => { const txn = prepareWalletTx( buildSdkTx({ type: 'pay', @@ -110,18 +84,18 @@ describe('Error Use cases', () => { }); }, unsignedTransactions) ).resolves.toMatchObject({ - message: expect.stringContaining('[RequestError.UserRejected]'), + message: expect.stringContaining('The extension user does not'), code: 4001, }); }); - test('Error on Missing account', async () => { - const invalidAccount = accounts.ui; + test('Error on Sender not imported to AlgoSigner', async () => { + const invalidAccount = accounts.ui.address; const txn = prepareWalletTx( buildSdkTx({ type: 'pay', - from: invalidAccount.address, - to: invalidAccount.address, + from: invalidAccount, + to: invalidAccount, amount: Math.ceil(Math.random() * 1000), ...ledgerParams, fee: 1000, @@ -143,17 +117,17 @@ describe('Error Use cases', () => { message: expect.stringContaining('There was a problem signing the transaction(s).'), code: 4100, name: expect.stringContaining('AlgoSignerRequestError'), - data: expect.stringContaining(accounts.ui.address), + data: expect.stringContaining(invalidAccount), }); }); + // Signers validations test('Error on Empty signers for Single transactions', async () => { - const invalidAccount = accounts.ui; const txn = prepareWalletTx( buildSdkTx({ type: 'pay', - from: invalidAccount.address, - to: invalidAccount.address, + from: account1.address, + to: account1.address, amount: Math.ceil(Math.random() * 1000), ...ledgerParams, fee: 1000, @@ -173,84 +147,91 @@ describe('Error Use cases', () => { }); }, unsignedTransactions) ).resolves.toMatchObject({ - message: expect.stringContaining('Signers array should only'), + message: expect.stringContaining('There are no transactions to sign'), code: 4300, name: expect.stringContaining('AlgoSignerRequestError'), }); }); - // // @TODO: Wallet Transaction Structure check tests -}); - -describe('Multisig Transaction Use cases', () => { - test('Sign MultiSig Transaction with All Accounts', async () => { - const multisigTxn = prepareWalletTx( + test('Error on Single signer not matching the sender', async () => { + const txn = prepareWalletTx( buildSdkTx({ type: 'pay', - from: msigAccount.address, - to: msigAccount.address, + from: account1.address, + to: account1.address, amount: Math.ceil(Math.random() * 1000), ...ledgerParams, fee: 1000, }) ); - multisigTxn.msig = { - version: 1, - threshold: 2, - addrs: msigAccount.subaccounts.map((acc) => acc.address), - }; + txn.signers = [account2.address]; + unsignedTransactions = [txn]; - unsignedTransactions = [multisigTxn]; - const signedTransactions = await signTxn(unsignedTransactions, async () => { - const popup = await getPopup(); - const tooltipText = await popup.evaluate(() => { - return getComputedStyle( - document.querySelector('[data-tooltip]'), - '::before' - ).getPropertyValue('content'); - }); - await expect(tooltipText).toContain('Multisignature'); + await expect( + dappPage.evaluate((transactions) => { + return Promise.resolve(AlgoSigner.signTxn(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions) + ).resolves.toMatchObject({ + message: expect.stringContaining('There was a problem signing the transaction(s).'), + code: 4300, + name: expect.stringContaining('AlgoSignerRequestError'), + data: expect.stringContaining("When a single-address 'signers'"), }); - - // Verify signature is added - const decodedTransaction = decodeBase64Blob(signedTransactions[0].blob); - expect(decodedTransaction).toHaveProperty('txn'); - expect(decodedTransaction).toHaveProperty('msig'); - expect(decodedTransaction.msig).toHaveProperty('subsig'); - expect(decodedTransaction.msig.subsig).toHaveLength(3); - expect(decodedTransaction.msig.subsig[0]).toHaveProperty('s'); - expect(decodedTransaction.msig.subsig[1]).toHaveProperty('s'); - expect(decodedTransaction.msig.subsig[2]).not.toHaveProperty('s'); }); - test('Sign MultiSig Transaction with Specific Signer', async () => { - unsignedTransactions[0].signers = [account1.address]; - const signedTransactions = await signTxn(unsignedTransactions); + test('Error on Single signer not matching authAddr', async () => { + const txn = prepareWalletTx( + buildSdkTx({ + type: 'pay', + from: account1.address, + to: account1.address, + amount: Math.ceil(Math.random() * 1000), + ...ledgerParams, + fee: 1000, + }) + ); + txn.signers = [account1.address]; + txn.authAddr = account2.address; + unsignedTransactions = [txn]; - // Verify correct signature is added - const decodedTransaction = decodeBase64Blob(signedTransactions[0].blob); - expect(decodedTransaction).toHaveProperty('txn'); - expect(decodedTransaction).toHaveProperty('msig'); - expect(decodedTransaction.msig).toHaveProperty('subsig'); - expect(decodedTransaction.msig.subsig).toHaveLength(3); - expect(decodedTransaction.msig.subsig[0]).toHaveProperty('s'); - expect(decodedTransaction.msig.subsig[1]).not.toHaveProperty('s'); - expect(decodedTransaction.msig.subsig[2]).not.toHaveProperty('s'); + await expect( + dappPage.evaluate((transactions) => { + return Promise.resolve(AlgoSigner.signTxn(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions) + ).resolves.toMatchObject({ + message: expect.stringContaining('There was a problem signing the transaction(s).'), + code: 4300, + name: expect.stringContaining('AlgoSignerRequestError'), + data: expect.stringContaining("When a single-address 'signers'"), + }); }); -}); -describe('Group Transactions Use cases', () => { - test('Reject on incomplete Group', async () => { - const txn = buildSdkTx({ - type: 'pay', - from: account1.address, - to: account1.address, - amount: Math.ceil(Math.random() * 1000), - ...ledgerParams, - fee: 1000, - }); - const groupedTransactions = algosdk.assignGroupID([txn, txn]); - unsignedTransactions = [prepareWalletTx(groupedTransactions[0])]; + test('Error on Invalid signer address', async () => { + const fakeAccount = 'THISSIGNERDOESNTEXIST'; + const txn = prepareWalletTx( + buildSdkTx({ + type: 'pay', + from: account1.address, + to: account1.address, + amount: Math.ceil(Math.random() * 1000), + ...ledgerParams, + fee: 1000, + }) + ); + txn.signers = [fakeAccount]; + unsignedTransactions = [txn]; await expect( dappPage.evaluate((transactions) => { @@ -263,79 +244,53 @@ describe('Group Transactions Use cases', () => { }); }, unsignedTransactions) ).resolves.toMatchObject({ - message: expect.stringContaining('group is incomplete'), + message: expect.stringContaining('There was a problem signing the transaction(s).'), code: 4300, name: expect.stringContaining('AlgoSignerRequestError'), + data: expect.stringContaining(`Signers array contains the invalid address "${fakeAccount}"`), }); }); - test('Accept Group ID for Single Transactions', async () => { - const txn = buildSdkTx({ - type: 'pay', - from: account1.address, - to: account1.address, - amount: Math.ceil(Math.random() * 1000), - ...ledgerParams, - fee: 1000, - }); - const groupedTransactions = algosdk.assignGroupID([txn]); - unsignedTransactions = [prepareWalletTx(groupedTransactions[0])]; - - const signedTransactions = await signTxn(unsignedTransactions); - await expect(signedTransactions[0]).not.toBeNull(); - }); + // AuthAddr validations + test('Error on Invalid authAddr', async () => { + const fakeAccount = 'THISAUTHADDRDOESNTEXIST'; + const txn = prepareWalletTx( + buildSdkTx({ + type: 'pay', + from: account1.address, + to: account1.address, + amount: Math.ceil(Math.random() * 1000), + ...ledgerParams, + fee: 1000, + }) + ); + txn.authAddr = fakeAccount; + unsignedTransactions = [txn]; - test('Group Transaction with Reference Transaction && Pooled Fee', async () => { - const tx1 = buildSdkTx({ - type: 'pay', - from: account1.address, - to: account2.address, - amount: Math.ceil(Math.random() * 1000), - ...ledgerParams, - fee: 1000, - }); - const tx2 = buildSdkTx({ - type: 'pay', - from: account2.address, - to: account1.address, - amount: Math.ceil(Math.random() * 1000), - ...ledgerParams, - }); - const tx3 = buildSdkTx({ - type: 'pay', - from: account1.address, - to: account2.address, - amount: Math.ceil(Math.random() * 1000), - ...ledgerParams, + await expect( + dappPage.evaluate((transactions) => { + return Promise.resolve(AlgoSigner.signTxn(transactions)) + .then((data) => { + return data; + }) + .catch((error) => { + return error; + }); + }, unsignedTransactions) + ).resolves.toMatchObject({ + message: expect.stringContaining('There was a problem signing the transaction(s).'), + code: 4300, + name: expect.stringContaining('AlgoSignerRequestError'), + data: expect.stringContaining(`'authAddr' contains the invalid address "${fakeAccount}"`), }); - - unsignedTransactions = await algosdk.assignGroupID([tx1, tx2, tx3]); - unsignedTransactions = unsignedTransactions.map((txn) => prepareWalletTx(txn)); - unsignedTransactions[2].signers = []; - - const signedTransactions = await signTxn(unsignedTransactions); - await expect(signedTransactions[2]).toBeNull(); - await expect(signedTransactions.filter((i) => i)).toHaveLength(2); }); - test('Max # of Transactions on Group', async () => { - const tx = buildSdkTx({ - type: 'pay', - from: account1.address, - to: account2.address, - amount: Math.ceil(Math.random() * 1000), - ...ledgerParams, - fee: 1000, - }); - - const txArray = []; - for (let i = 0; i < 16; i++) { - txArray.push(tx); - } - - unsignedTransactions = await algosdk.assignGroupID(txArray); - unsignedTransactions[16] = unsignedTransactions[0]; - unsignedTransactions = unsignedTransactions.map((txn) => prepareWalletTx(txn)); + // Msig validations + test('Error on Msig Signer not imported to AlgoSigner', async () => { + const invalidAccount = msigAccount.subaccounts[2].address; + const txn = { ...msigTxn }; + txn.signers = [account1.address, invalidAccount]; + unsignedTransactions = [txn]; await expect( dappPage.evaluate((transactions) => { @@ -348,11 +303,53 @@ describe('Group Transactions Use cases', () => { }); }, unsignedTransactions) ).resolves.toMatchObject({ - message: expect.stringContaining('16 transactions at a time'), - code: 4201, + message: expect.stringContaining('There was a problem signing the transaction(s).'), + code: 4300, name: expect.stringContaining('AlgoSignerRequestError'), + data: expect.stringContaining(invalidAccount), + }); + }); + + // // @TODO: Wallet Transaction Structure check tests +}); + +describe('Multisig Transaction Use cases', () => { + test('Sign MultiSig Transaction with All Accounts', async () => { + unsignedTransactions = [msigTxn]; + const signedTransactions = await signDappTxns(unsignedTransactions, async () => { + const popup = await getPopup(); + const tooltipText = await popup.evaluate(() => { + return getComputedStyle( + document.querySelector('[data-tooltip]'), + '::before' + ).getPropertyValue('content'); + }); + await expect(tooltipText).toContain('Multisignature'); }); + + // Verify signature is added + const decodedTransaction = decodeBase64Blob(signedTransactions[0].blob); + expect(decodedTransaction).toHaveProperty('txn'); + expect(decodedTransaction).toHaveProperty('msig'); + expect(decodedTransaction.msig).toHaveProperty('subsig'); + expect(decodedTransaction.msig.subsig).toHaveLength(3); + expect(decodedTransaction.msig.subsig[0]).toHaveProperty('s'); + expect(decodedTransaction.msig.subsig[1]).toHaveProperty('s'); + expect(decodedTransaction.msig.subsig[2]).not.toHaveProperty('s'); }); - // @TODO: Add errors for mismatches, incomplete groups, etc + test('Sign MultiSig Transaction with Specific Signer', async () => { + unsignedTransactions[0].signers = [account1.address]; + const signedTransactions = await signDappTxns(unsignedTransactions); + + // Verify correct signature is added + const decodedTransaction = decodeBase64Blob(signedTransactions[0].blob); + expect(decodedTransaction).toHaveProperty('txn'); + expect(decodedTransaction).toHaveProperty('msig'); + expect(decodedTransaction.msig).toHaveProperty('subsig'); + expect(decodedTransaction.msig.subsig).toHaveLength(3); + expect(decodedTransaction.msig.subsig[0]).toHaveProperty('s'); + expect(decodedTransaction.msig.subsig[1]).not.toHaveProperty('s'); + expect(decodedTransaction.msig.subsig[2]).not.toHaveProperty('s'); + }); }); From da394f9a2ab1c0237fb22e8313751d01fd9741a8 Mon Sep 17 00:00:00 2001 From: Jan Marcano Date: Wed, 5 Oct 2022 12:51:31 -0300 Subject: [PATCH 4/6] Update dApp documentation for 'stxn' --- docs/dApp-integration.md | 79 +++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/docs/dApp-integration.md b/docs/dApp-integration.md index a0de00dd..f093e23d 100644 --- a/docs/dApp-integration.md +++ b/docs/dApp-integration.md @@ -161,6 +161,7 @@ Transactions objects need to be presented with the following structure: { txn: Base64-encoded string of a transaction binary, signers?: [optional] array of addresses to sign with (defaults to the sender), + stxn?: [optional] Base64-encoded string of a signed transaction binary multisig?: [optional] extra metadata needed for multisig transactions, }; ``` @@ -219,6 +220,7 @@ let signedTxs = await AlgoSigner.signTxn([ }, ]); ``` + Alternatively, you can provide multiple arrays of transactions at once. Same rules regarding the contents of the groups apply. **Request** @@ -237,6 +239,7 @@ AlgoSigner.signTxn([ ], ]); ``` + **Response** ```json @@ -266,7 +269,7 @@ let binarySignedTx = AlgoSigner.encoding.base64ToMsgpack(signedTxs[0].blob); await client.sendRawTransaction(binarySignedTx).do(); ``` -#### Atomic Transactions +### Atomic Transactions For Atomic transactions, provide an array of transaction objects with the same group ID, _provided in the same order as when the group was assigned_. @@ -309,11 +312,25 @@ let binarySignedTxs = signedTxs.map((tx) => AlgoSigner.encoding.base64ToMsgpack( await client.sendRawTransaction(binarySignedTxs).do(); ``` -In case not all group transactions belong to accounts on AlgoSigner, you can set the `signers` field of the transaction object as an empty array to specify that it's only being sent to AlgoSigner for reference and group validation, not for signing. +#### Reference Atomic transactions + +In case not all group transactions belong to accounts on AlgoSigner, you can set the `signers` field of the transaction object as an empty array to specify that it's only being sent to AlgoSigner for reference and group validation, not for signing. Reference transactions should look like this: + +```js +{ + txn: 'B64_TXN', + signers: [], + // This tells AlgoSigner that this transaction is not meant to be signed +} +``` + +`AlgoSigner.signTxn()` will return `null` in the position(s) where reference transactions were provided. In these instances, you'd have to sign the missing transaction(s) by your own means before they can be sent. This is useful for transactions that require external signing, like `lsig` transactions. -_AlgoSigner.signTxn()_ will return _null_ in it's response array for the positions were reference transactions were sent. +#### Providing Signed reference transaction(s) -In these cases, you'd have to sign the missing transaction by your own means before it can be sent (by using the SDK, for instance). +You can provide a signed reference transaction to AlgoSigner via the `stxn` field of the transaction object for it to be validated and returned as part of the group. For the transaction(s) where `stxn` was provided, `AlgoSigner.signTxn()` will return the `stxn` string in the same position of the response array as the corresponding reference transaction(s) instead of `null`. + +**Example** ```js let tx1 = new algosdk.Transaction({ @@ -338,17 +355,61 @@ let signedTxs = await AlgoSigner.signTxn([ txn: base64Txs[0], }, { - // This tells AlgoSigner that this transaction is not meant to be signed txn: base64Txs[1], signers: [], + stxn: 'MANUALLY_SIGNED_SECOND_TXN_B64', }, ]); ``` -Signing the remaining transaction with the SDK would look like this: +**Response** + +```js +[ + { + txID: '...', + blob: 'ALGOSIGNER_SIGNED_B64', + }, + { + txID: '...', + blob: 'MANUALLY_SIGNED_SECOND_TXN_B64', + }, +]; +``` + +#### Signing reference transactions manually + +In case you can't or don't want to provide the `stxn`, the provided transaction should look like this: + +**Example** + +```js +let signedTxs = await AlgoSigner.signTxn([ + { + txn: base64Txs[0], + }, + { + txn: base64Txs[1], + signers: [], + }, +]); +``` + +**Response** + +```js +[ + { + txID: '...', + blob: 'ALGOSIGNER_SIGNED_B64', + }, + null, +]; +``` + +Afterwards, you can sign and send the remaining transaction(s) with the SDK: ```js -// The AlgoSigner.signTxn() response would look like '[{ txID, blob }, null]' // Convert first transaction to binary from the response let signedTx1Binary = AlgoSigner.encoding.base64ToMsgpack(signedTxs[0].blob); // Sign leftover transaction with the SDK @@ -358,7 +419,7 @@ let signedTx2Binary = tx2.signTxn(externalAccount.sk); await client.sendRawTransaction([signedTx1Binary, signedTx2Binary]).do(); ``` -Alternatively, if you're using the [AlgoSigner.send()](#algosignersend-ledger-mainnettestnet-txblob-) to send the transaction, you have to merge the binaries before converting to a base64 encoded string. +Alternatively, if you're using the [AlgoSigner.send()](#algosignersend-ledger-mainnettestnet-txblob-) to send the transaction(s), you have to merge the binaries before converting to a single base64 encoded string. ```js // Merge transaction binaries into a single Uint8Array @@ -469,7 +530,7 @@ AlgoSigner.send({ AlgoSigner may return some of the following error codes when requesting signatures: | Error Code | Description | Additional notes | -| ----------- | ----------- | --------------- | +| ---------- | ----------- | ---------------- | | 4000 | An unknown error occured. | N/A | | 4001 | The user rejected the signature request. | N/A | | 4100 | The requested operation and/or account has not been authorized by the user. | This is usually due to the connection between the dApp and the wallet becoming stale and the user [needing to reconnect](connection-issues.md). Otherwise, it may signal that you are trying to sign with private keys not found on AlgoSigner. | From cb907c8700ee20e261cc41165308d5972325f21d Mon Sep 17 00:00:00 2001 From: Jan Marcano Date: Wed, 5 Oct 2022 13:14:09 -0300 Subject: [PATCH 5/6] Add e2e test for 'stxn' --- packages/test-project/tests/dapp-groups.test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/test-project/tests/dapp-groups.test.js b/packages/test-project/tests/dapp-groups.test.js index e720103a..85ad7caa 100644 --- a/packages/test-project/tests/dapp-groups.test.js +++ b/packages/test-project/tests/dapp-groups.test.js @@ -68,6 +68,8 @@ describe('Wallet Setup', () => { }); describe('Group Transactions Use cases', () => { + let signedTxn; + test('Reject on incomplete Group', async () => { tx1 = buildSdkTx({ type: 'pay', @@ -144,6 +146,20 @@ describe('Group Transactions Use cases', () => { const signedTransactions = await signDappTxns(unsignedTransactions); await expect(signedTransactions[2]).toBeNull(); await expect(signedTransactions.filter((i) => i)).toHaveLength(2); + + // For next test + signedTxn = signedTransactions[1]; + }); + + test('Provide "stxn" for Reference Transaction', async () => { + const unsignedTransactions = [tx1, tx2, tx3].map((txn) => prepareWalletTx(txn)); + unsignedTransactions[1].signers = []; + unsignedTransactions[1].stxn = signedTxn.blob; + + const signedTransactions = await signDappTxns(unsignedTransactions); + await expect(signedTransactions[1]).toStrictEqual(signedTxn); + await expect(signedTransactions[2]).not.toBeNull(); + await expect(signedTransactions.filter((i) => i)).toHaveLength(3); }); test('Max # of Transactions on Group', async () => { From f9b8e15ce8ea73d43b129ff3d892bd09793245cd Mon Sep 17 00:00:00 2001 From: Jan Marcano Date: Wed, 5 Oct 2022 13:14:45 -0300 Subject: [PATCH 6/6] Bump version to 1.9.6 --- README.md | 3 ++- package.json | 2 +- packages/common/package.json | 2 +- packages/crypto/package.json | 2 +- packages/dapp/package.json | 2 +- packages/extension/manifest.json | 2 +- packages/extension/package.json | 2 +- packages/storage/package.json | 2 +- packages/test-project/package.json | 2 +- packages/ui/package.json | 2 +- 10 files changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 0e3d5e96..95de4601 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Developers working with dApps may also install directly from the release package An interactive transition guide is available [here](https://purestake.github.io/algosigner-dapp-example/v1v2TransitionGuide.html). -## 1.9.5 Release +## 1.9.6 Release ### Main updates This update adds supports for easier transfers with the new autocomplete feature. Start typing an account, contact name or name service alias on the destination field when sending Algos or ASAs and you'll be able to select the desired address from a dropdown. This also marks the end of the support of the older signing methods that were previously available. @@ -24,6 +24,7 @@ This update adds supports for easier transfers with the new autocomplete feature - External name services (NFDomains and Algorand Namespace Service) - `AlgoSigner.sign()` and `AlgoSigner.signMultisig()` have been deprecated - New Account creation now occurs in the browser, improving ease of use when saving the mnemonic +- Improved dApp support with the new [`stxn`](docs/dApp-integration.md#providing-signed-reference-transactions) field, as well as new and more descriptive error types ### Other updates - Improved Account Importing and Cache Clearing diff --git a/package.json b/package.json index c11adcb8..d9833a52 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "algosigner", - "version": "1.9.5", + "version": "1.9.6", "author": "https://developer.purestake.io", "description": "Sign Algorand transactions in your browser with PureStake.", "repository": "https://github.com/PureStake/algosigner", diff --git a/packages/common/package.json b/packages/common/package.json index 6da1ba55..03ed3769 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@algosigner/common", - "version": "1.9.5", + "version": "1.9.6", "author": "https://developer.purestake.io", "description": "Common library functions for AlgoSigner.", "repository": "https://github.com/PureStake/algosigner", diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 3611b52a..96d41b21 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-crypto", - "version": "1.9.5", + "version": "1.9.6", "author": "https://developer.purestake.io", "description": "Cryptographic wrapper for saving and retrieving extention information in AlgoSigner.", "repository": { diff --git a/packages/dapp/package.json b/packages/dapp/package.json index 0f032fcd..143f3f23 100644 --- a/packages/dapp/package.json +++ b/packages/dapp/package.json @@ -1,6 +1,6 @@ { "name": "@algosigner/dapp", - "version": "1.9.5", + "version": "1.9.6", "author": "https://developer.purestake.io", "repository": "https://github.com/PureStake/algosigner", "license": "MIT", diff --git a/packages/extension/manifest.json b/packages/extension/manifest.json index 3b5e6709..231f6621 100644 --- a/packages/extension/manifest.json +++ b/packages/extension/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "AlgoSigner", "author": "https://developer.purestake.io", - "version": "1.9.5", + "version": "1.9.6", "description": "Algorand Wallet Extension | Send & Receive ALGOs | Sign dApp Transactions", "icons": { "48": "icon.png" diff --git a/packages/extension/package.json b/packages/extension/package.json index e6d18f1a..b0db3e3e 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-extension", - "version": "1.9.5", + "version": "1.9.6", "author": "https://developer.purestake.io", "repository": "https://github.com/PureStake/algosigner", "license": "MIT", diff --git a/packages/storage/package.json b/packages/storage/package.json index ad1cd8b5..56172ecd 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-storage", - "version": "1.9.5", + "version": "1.9.6", "author": "https://developer.purestake.io", "repository": "https://github.com/PureStake/algosigner", "license": "MIT", diff --git a/packages/test-project/package.json b/packages/test-project/package.json index 5309a755..1cbf1e0d 100644 --- a/packages/test-project/package.json +++ b/packages/test-project/package.json @@ -1,6 +1,6 @@ { "name": "algorand-test-project", - "version": "1.9.5", + "version": "1.9.6", "repository": "https://github.com/PureStake/algosigner", "license": "MIT", "description": "Repository for tests", diff --git a/packages/ui/package.json b/packages/ui/package.json index c33354c1..99d3a2cf 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "algosigner-ui", - "version": "1.9.5", + "version": "1.9.6", "author": "https://developer.purestake.io", "repository": "https://github.com/PureStake/algosigner", "license": "MIT",