diff --git a/packages/common/src/common.test.ts b/packages/common/src/common.test.ts index 09a2402d..93291e2c 100644 --- a/packages/common/src/common.test.ts +++ b/packages/common/src/common.test.ts @@ -1,7 +1,8 @@ import { extensionBrowser } from './chrome'; import { logging } from './logging'; import { isFromExtension } from './utils'; -import { RequestError, Transaction, TAccount, Note, Amount } from './types'; +import { Transaction, TAccount, Note, Amount } from './types'; +import { RequestError } from './errors'; test('Test chrome reference', () => { //@ts-ignore @@ -17,13 +18,6 @@ test('Test extension parts', () => { expect(isFromExtension('chrome-extension://12345')).toBe(true); }); -test('Type - RequestErrors undefined test', () => { - expect(RequestError.Undefined).toMatchObject({ - message: '[RequestError.Undefined] An undefined error occurred.', - code: 4000, - }); -}); - test('Type - Transaction', () => { expect({ amount: 1, from: 'AAA', to: 'BBB' } as Transaction).toBeInstanceOf(Object); }); @@ -39,3 +33,27 @@ test('Type - Note', () => { test('Type - Amount', () => { expect(12345 as Amount).toBe(12345); }); + +test('RequestError - Structure', () => { + const address = 'MTHFSNXBMBD4U46Z2HAYAOLGD2EV6GQBPXVTL727RR3G44AJ3WVFMZGSBE'; + const ledger = 'MainNet'; + const testError = RequestError.NoAccountMatch(address, ledger); + + expect(testError).toMatchObject({ + message: `No matching account found on AlgoSigner for address: "${address}" on network ${ledger}.`, + code: 4100, + }); + + expect(RequestError.SigningError(4000, [testError])).toMatchObject({ + message: 'There was a problem signing the transaction(s).', + code: 4000, + data: [testError], + }); +}); + +test('RequestError - Throwing', () => { + function throwError() { + throw RequestError.Undefined; + } + expect(() => throwError()).toThrow(RequestError.Undefined); +}); \ No newline at end of file diff --git a/packages/common/src/errors.ts b/packages/common/src/errors.ts new file mode 100644 index 00000000..3afe8b07 --- /dev/null +++ b/packages/common/src/errors.ts @@ -0,0 +1,96 @@ +export class RequestError { + // Maximum amount of transactions supported on a single group + static MAX_GROUP_SIZE = 16; + + message: string; + code: number; + name: string; + data?: any; + + static None = new RequestError('', 0); + static Undefined = new RequestError( + '[RequestError.Undefined] An undefined error occurred.', + 4000 + ); + static UserRejected = new RequestError( + '[RequestError.UserRejected] The extension user does not authorize the request.', + 4001 + ); + static NotAuthorizedByUser = new RequestError( + '[RequestError.NotAuthorized] The extension user does not authorize the request.', + 4100 + ); + static NoAccountMatch = (address: string, ledger: string): RequestError => + new RequestError( + `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.', + 4200 + ); + static PendingTransaction = new RequestError('Another query processing', 4201); + static LedgerMultipleTransactions = new RequestError( + 'Ledger hardware device signing not available for multiple transactions.', + 4201 + ); + static TooManyTransactions = new RequestError( + `The ledger does not support signing more than ${RequestError.MAX_GROUP_SIZE} transactions at a time.`, + 4201 + ); + static InvalidFields = (data?: any) => + new RequestError('Validation failed for transaction due to invalid properties.', 4300, data); + static InvalidTransactionStructure = (data?: any) => + 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).', + 4300 + ); + static InvalidStructure = new RequestError( + "The provided transaction object doesn't adhere to the correct structure.", + 4300 + ); + static InvalidMsigStructure = new RequestError( + "The provided multisig data doesn't adhere to the correct structure.", + 4300 + ); + static IncompleteOrDisorderedGroup = new RequestError( + 'The transaction group is incomplete or presented in a different order than when it was created.', + 4300 + ); + static NonMatchingGroup = new RequestError( + 'All transactions need to belong to the same group.', + 4300 + ); + static NoDifferentLedgers = new RequestError( + 'All transactions need to belong to the same ledger.', + 4300 + ); + static SigningError = (code: number, data?: any) => + new RequestError('There was a problem signing the transaction(s).', code, data); + + protected constructor(message: string, code: number, data?: any) { + this.name = 'AlgoSignerRequestError'; + this.message = message; + this.code = code; + this.data = data; + + Error.captureStackTrace(this, RequestError); + } +} diff --git a/packages/common/src/messaging/types.ts b/packages/common/src/messaging/types.ts index cf97d40d..9fde3e67 100644 --- a/packages/common/src/messaging/types.ts +++ b/packages/common/src/messaging/types.ts @@ -10,7 +10,6 @@ export enum JsonRpcMethod { AuthorizationAllow = 'authorization-allow', AuthorizationDeny = 'authorization-deny', SignAllow = 'sign-allow', - SignAllowMultisig = 'sign-allow-multisig', SignAllowWalletTx = 'sign-allow-wallet-tx', SignDeny = 'sign-deny', SignWalletTransaction = 'sign-wallet-transaction', @@ -35,7 +34,6 @@ export enum JsonRpcMethod { AssetDetails = 'asset-details', AssetsAPIList = 'assets-api-list', AssetsVerifiedList = 'assets-verified-list', - AssetOptOut = 'asset-opt-out', SignSendTransaction = 'sign-send-transaction', ChangeLedger = 'change-ledger', SaveNetwork = 'save-network', diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 9bc42667..e58edf09 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -1,48 +1,4 @@ /* eslint-disable no-unused-vars */ -export class RequestError { - message: string; - code: number; - name?: string; - data?: any; - - static None = new RequestError('', 0); - static Undefined = new RequestError( - '[RequestError.Undefined] An undefined error occurred.', - 4000 - ); - static UserRejected = new RequestError( - '[RequestError.UserRejected] The extension user does not authorize the request.', - 4001 - ); - static NotAuthorizedByUser = new RequestError( - '[RequestError.NotAuthorized] The extension user does not authorize the request.', - 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 InvalidFormat = new RequestError( - '[RequestError.InvalidFormat] Please provide an array of either valid transaction objects or nested arrays of valid transaction objects.', - 4300 - ); - - protected constructor(message: string, code: number, name?: string, data?: any) { - this.message = message; - this.code = code; - this.name = name; - this.data = data; - } -} - export type Field = string | number; export type TAccount = Field; diff --git a/packages/dapp/src/fn/interfaces.ts b/packages/dapp/src/fn/interfaces.ts index bd10f19a..40d70b55 100644 --- a/packages/dapp/src/fn/interfaces.ts +++ b/packages/dapp/src/fn/interfaces.ts @@ -1,4 +1,5 @@ -import { RequestError, WalletTransaction } from '@algosigner/common/types'; +import { WalletTransaction } from '@algosigner/common/types'; +import { RequestError } from '@algosigner/common/errors'; import { JsonPayload } from '@algosigner/common/messaging/types'; /* eslint-disable no-unused-vars */ diff --git a/packages/dapp/src/fn/task.test.ts b/packages/dapp/src/fn/task.test.ts index f1a437e3..301b44a2 100644 --- a/packages/dapp/src/fn/task.test.ts +++ b/packages/dapp/src/fn/task.test.ts @@ -1,6 +1,6 @@ import { Task } from './task'; import { MessageBuilder } from '../messaging/builder'; -import { RequestError } from '@algosigner/common/types'; +import { RequestError } from '@algosigner/common/errors'; import { JsonRpcMethod } from '@algosigner/common/messaging/types'; jest.mock('../messaging/builder'); diff --git a/packages/dapp/src/fn/task.ts b/packages/dapp/src/fn/task.ts index 5741a996..ae3be41e 100644 --- a/packages/dapp/src/fn/task.ts +++ b/packages/dapp/src/fn/task.ts @@ -2,11 +2,8 @@ import { ITask } from './interfaces'; import { MessageBuilder } from '../messaging/builder'; -import { - Transaction, - RequestError, - WalletTransaction, -} from '@algosigner/common/types'; +import { Transaction, WalletTransaction } from '@algosigner/common/types'; +import { RequestError } from '@algosigner/common/errors'; import { JsonRpcMethod, JsonPayload } from '@algosigner/common/messaging/types'; import { Runtime } from '@algosigner/common/runtime/runtime'; @@ -53,12 +50,11 @@ export class Task extends Runtime implements ITask { if ( txOrGroup === null || txOrGroup === undefined || - (!Array.isArray(txOrGroup) && typeof txOrGroup === 'object' && - (!txOrGroup.txn || (txOrGroup.txn && !txOrGroup.txn.length)) - ) || - (Array.isArray(txOrGroup) && - (!txOrGroup.length || (txOrGroup.length && !txOrGroup.every((tx) => tx !== null))) - ) + (!Array.isArray(txOrGroup) && + typeof txOrGroup === 'object' && + (!txOrGroup.txn || (txOrGroup.txn && !txOrGroup.txn.length))) || + (Array.isArray(txOrGroup) && + (!txOrGroup.length || (txOrGroup.length && !txOrGroup.every((tx) => tx !== null)))) ) throw formatError; }); diff --git a/packages/dapp/src/messaging/builder.ts b/packages/dapp/src/messaging/builder.ts index 3eb85e7f..6346c150 100644 --- a/packages/dapp/src/messaging/builder.ts +++ b/packages/dapp/src/messaging/builder.ts @@ -1,4 +1,4 @@ -import { RequestError } from '@algosigner/common/types'; +import { RequestError } from '@algosigner/common/errors'; import { JsonRpcMethod, JsonPayload } from '@algosigner/common/messaging/types'; import { JsonRpc } from '@algosigner/common/messaging/jsonrpc'; diff --git a/packages/dapp/src/messaging/handler.ts b/packages/dapp/src/messaging/handler.ts index 665ac618..fe5c558c 100644 --- a/packages/dapp/src/messaging/handler.ts +++ b/packages/dapp/src/messaging/handler.ts @@ -1,5 +1,5 @@ import { OnMessageListener } from './types'; -import { RequestError } from '@algosigner/common/types'; +import { RequestError } from '@algosigner/common/errors'; export class OnMessageHandler { static promise(resolve: Function, reject: Function): OnMessageListener { diff --git a/packages/extension/src/background/messaging/handler.ts b/packages/extension/src/background/messaging/handler.ts index 4da63a1b..fa55ca60 100644 --- a/packages/extension/src/background/messaging/handler.ts +++ b/packages/extension/src/background/messaging/handler.ts @@ -2,7 +2,7 @@ import { MessageApi } from './api'; import { Task } from './task'; import encryptionWrap from '../encryptionWrap'; import { isFromExtension } from '@algosigner/common/utils'; -import { RequestError } from '@algosigner/common/types'; +import { RequestError } from '@algosigner/common/errors'; import { JsonRpcMethod, MessageSource } from '@algosigner/common/messaging/types'; import logging from '@algosigner/common/logging'; diff --git a/packages/extension/src/background/messaging/internalMethods.ts b/packages/extension/src/background/messaging/internalMethods.ts index 4033bd65..20de3930 100644 --- a/packages/extension/src/background/messaging/internalMethods.ts +++ b/packages/extension/src/background/messaging/internalMethods.ts @@ -2,7 +2,8 @@ import algosdk from 'algosdk'; import { JsonRpcMethod } from '@algosigner/common/messaging/types'; import { logging } from '@algosigner/common/logging'; import { ExtensionStorage } from '@algosigner/storage/src/extensionStorage'; -import { Alias, Ledger, Namespace, NamespaceConfig, RequestError } from '@algosigner/common/types'; +import { Alias, Ledger, Namespace, NamespaceConfig } from '@algosigner/common/types'; +import { RequestError } from '@algosigner/common/errors'; import { AliasConfig } from '@algosigner/common/config'; import { Task } from './task'; import { API, Cache } from './types'; @@ -20,7 +21,6 @@ import { import { BaseValidatedTxnWrap } from '../transaction/baseValidatedTxnWrap'; import { buildTransaction } from '../utils/transactionBuilder'; import { getBaseSupportedLedgers, LedgerTemplate } from '@algosigner/common/types/ledgers'; -import { NoAccountMatch } from '../../errors/transactionSign'; import { extensionBrowser } from '@algosigner/common/chrome'; const session = new Session(); @@ -107,7 +107,7 @@ export class InternalMethods { break; } } - if (!found) throw new NoAccountMatch(address, ledger); + if (!found) throw RequestError.NoAccountMatch(address, ledger); } private static loadAccountAssetsDetails(address: string, ledger: Ledger) { @@ -373,7 +373,9 @@ export class InternalMethods { if (existingAccounts) { for (let i = 0; i < existingAccounts.length; i++) { if (existingAccounts[i].address === targetAddress) { - throw new Error(`An account with this address already exists in your ${ledger} wallet.`); + throw new Error( + `An account with this address already exists in your ${ledger} wallet.` + ); } if (existingAccounts[i].name === name) { throw new Error(`An account named '${name}' already exists in your ${ledger} wallet.`); @@ -432,9 +434,9 @@ export class InternalMethods { } public static [JsonRpcMethod.LedgerGetSessionTxn](request: any, sendResponse: Function) { - if (session.txnWrap && 'body' in session.txnWrap) { - // The transaction may contain source and JSONRPC info, the body.params will be the transaction validation object - sendResponse(session.txnWrap.body.params); + if (session.txnWrap) { + // The transaction contains source and JSONRPC info, the body.params will be the transaction validation object + sendResponse(session.txnWrap); } else { sendResponse({ error: 'Transaction not found in session.' }); } @@ -513,11 +515,18 @@ export class InternalMethods { sendResponse({ txId: resp.txId }); }) .catch((e: any) => { - if (e.message.includes('overspend')) + if (e.message.includes('overspend')) { sendResponse({ error: "Overspending. Your account doesn't have sufficient funds.", }); - else sendResponse({ error: e.message }); + } else if (e.message.includes('below min')) { + sendResponse({ + error: + 'Overspending. This transaction would bring your account below the minimum balance limit.', + }); + } else { + sendResponse({ error: e.message }); + } }); } else { sendResponse({ error: 'Session transaction does not match the signed transaction.' }); @@ -538,7 +547,7 @@ export class InternalMethods { // Explicitly using txnWrap on session instead of auth message for two reasons: // 1) So it lives inside background sandbox containment. // 2) The extension may close before a proper id on the new tab can allow the data to be saved. - session.txnWrap = request; + session.txnWrap = request.body.params; // Transaction wrap will contain response message if from dApp and structure will be different const ledger = getLedgerFromGenesisId(request.body.params.transaction.genesisID); @@ -794,8 +803,8 @@ export class InternalMethods { return true; } - public static [JsonRpcMethod.AssetOptOut](request: any, sendResponse: Function) { - const { ledger, address, passphrase, id } = request.body.params; + public static [JsonRpcMethod.SignSendTransaction](request: any, sendResponse: Function) { + const { ledger, address, passphrase, txnParams } = request.body.params; this._encryptionWrap = new encryptionWrap(passphrase); const algod = this.getAlgod(ledger); @@ -804,136 +813,21 @@ export class InternalMethods { sendResponse(unlockedValue); return false; } - let account; - const authAddr = await Task.getChainAuthAddress(request.body.params); - const signAddress = authAddr || address; - - // Find address to send algos from - for (var i = unlockedValue[ledger].length - 1; i >= 0; i--) { - if (unlockedValue[ledger][i].address === signAddress) { - account = unlockedValue[ledger][i]; - break; - } - } const params = await algod.getTransactionParams().do(); const txn = { - type: 'axfer', - from: address, - to: address, - closeRemainderTo: address, - assetIndex: id, + ...txnParams, + amount: BigInt(txnParams.amount), fee: params.fee, firstRound: params.firstRound, lastRound: params.lastRound, genesisID: params.genesisID, genesisHash: params.genesisHash, }; + if ('note' in txn) txn.note = new Uint8Array(Buffer.from(txn.note)); - let transactionWrap: BaseValidatedTxnWrap = undefined; - try { - transactionWrap = getValidatedTxnWrap(txn, txn['type']); - } catch (e) { - logging.log(`Validation failed. ${e}`); - sendResponse({ error: `Validation failed. ${e}` }); - return; - } - if (!transactionWrap) { - // We don't have a transaction wrap. We have an unknow error or extra fields, reject the transaction. - logging.log( - 'A transaction has failed because of an inability to build the specified transaction type.' - ); - sendResponse({ - error: - 'A transaction has failed because of an inability to build the specified transaction type.', - }); - return; - } else if ( - transactionWrap.validityObject && - Object.values(transactionWrap.validityObject).some( - (value) => value['status'] === ValidationStatus.Invalid - ) - ) { - // We have a transaction that contains fields which are deemed invalid. We should reject the transaction. - const e = - 'One or more fields are not valid. Please check and try again.\n' + - Object.values(transactionWrap.validityObject) - .filter((value) => value['status'] === ValidationStatus.Invalid) - .map((vo) => vo['info']); - sendResponse({ error: e }); - return; - } else if (!account.mnemonic) { - sendResponse({ error: RequestError.NotAuthorizedOnChain.message }); - } else { - // We have a transaction which does not contain invalid fields, - // but may still contain fields that are dangerous - // or ones we've flagged as needing to be reviewed. - // Perform a change based on if this is a ledger device account - if (account.isHardware) { - // TODO: Temporary workaround by adding min-fee for estimate calculations since it's not in the sdk get params. - params['min-fee'] = 1000; - calculateEstimatedFee(transactionWrap, params); - - // Pass the transaction wrap we can pass to the - // central sign ledger function for consistency - this[JsonRpcMethod.LedgerSignTransaction]( - { source: 'ui', body: { params: transactionWrap } }, - (response) => { - // We only have to worry about possible errors here so we can ignore the created tab - if ('error' in response) { - sendResponse(response); - } else { - // Respond with a 0 tx id so that the page knows not to try and show it. - sendResponse({ txId: 0 }); - } - } - ); - - // Return to close connection - return true; - } else { - // We can use a modified popup to allow the normal flow, but require extra scrutiny. - const recoveredAccount = algosdk.mnemonicToSecretKey(account.mnemonic); - let signedTxn; - try { - const builtTx = buildTransaction(txn); - signedTxn = { - txID: builtTx.txID().toString(), - blob: builtTx.signTxn(recoveredAccount.sk), - }; - } catch (e) { - sendResponse({ error: e.message }); - return false; - } - - algod - .sendRawTransaction(signedTxn.blob) - .do() - .then((resp: any) => { - sendResponse({ txId: resp.txId }); - }) - .catch((e: any) => { - sendResponse({ error: e.message }); - }); - } - } - }); - - return true; - } - - public static [JsonRpcMethod.SignSendTransaction](request: any, sendResponse: Function) { - const { ledger, address, passphrase, txnParams } = request.body.params; - this._encryptionWrap = new encryptionWrap(passphrase); - const algod = this.getAlgod(ledger); - - this._encryptionWrap.unlock(async (unlockedValue: any) => { - if ('error' in unlockedValue) { - sendResponse(unlockedValue); - return false; - } let account; - const authAddr = await Task.getChainAuthAddress(request.body.params); + const authAddr = await Task.getChainAuthAddress({ address, ledger }); const signAddress = authAddr || address; // Find address to send algos from @@ -944,26 +838,13 @@ export class InternalMethods { } } - const params = await algod.getTransactionParams().do(); - const txn = { - ...txnParams, - amount: BigInt(txnParams.amount), - fee: params.fee, - firstRound: params.firstRound, - lastRound: params.lastRound, - genesisID: params.genesisID, - genesisHash: params.genesisHash, - }; - - if ('note' in txn) txn.note = new Uint8Array(Buffer.from(txn.note)); - let transactionWrap: BaseValidatedTxnWrap = undefined; try { transactionWrap = getValidatedTxnWrap(txn, txn['type']); } catch (e) { - logging.log(`Validation failed. ${e}`); - sendResponse({ error: `Validation failed. ${e}` }); - return; + logging.log(`Validation failed. ${e.message}`); + sendResponse({ error: `Validation failed. ${e.message}` }); + return false; } if (!transactionWrap) { // We don't have a transaction wrap. We have an unknow error or extra fields, reject the transaction. @@ -974,7 +855,7 @@ export class InternalMethods { error: 'A transaction has failed because of an inability to build the specified transaction type.', }); - return; + return false; } else if ( transactionWrap.validityObject && Object.values(transactionWrap.validityObject).some( @@ -988,7 +869,7 @@ export class InternalMethods { .filter((value) => value['status'] === ValidationStatus.Invalid) .map((vo) => vo['info']); sendResponse({ error: e }); - return; + return false; } else { // We have a transaction which does not contain invalid fields, // but may still contain fields that are dangerous @@ -1033,23 +914,29 @@ export class InternalMethods { return false; } - algod + this.getAlgod(ledger) .sendRawTransaction(signedTxn.blob) .do() .then((resp: any) => { sendResponse({ txId: resp.txId }); }) .catch((e: any) => { - if (e.message.includes('overspend')) + if (e.message.includes('overspend')) { sendResponse({ error: "Overspending. Your account doesn't have sufficient funds.", }); - else sendResponse({ error: e.message }); + } else if (e.message.includes('below min')) { + sendResponse({ + error: + 'Overspending. This transaction would bring your account below the minimum balance limit.', + }); + } else { + sendResponse({ error: e.message }); + } }); } } }); - return true; } @@ -1354,7 +1241,9 @@ export class InternalMethods { clearTimeout(timerId); }; // We save the fetch request to later execute all in parallel - const apiCall = fetch(apiURL, { signal: controller.signal }).then(handleResponse); + const apiCall = fetch(apiURL, { signal: controller.signal }) + .then(handleResponse) + .catch(() => []); apiFetches.push(apiCall); } else { returnedAliasedAddresses[namespace] = aliasesMatchingInNamespace; diff --git a/packages/extension/src/background/messaging/task.ts b/packages/extension/src/background/messaging/task.ts index 562fc027..deba957d 100644 --- a/packages/extension/src/background/messaging/task.ts +++ b/packages/extension/src/background/messaging/task.ts @@ -1,13 +1,13 @@ import algosdk from 'algosdk'; -import { RequestError, WalletTransaction } from '@algosigner/common/types'; +import { WalletTransaction } from '@algosigner/common/types'; +import { RequestError } from '@algosigner/common/errors'; import { JsonRpcMethod } from '@algosigner/common/messaging/types'; import { Ledger } from '@algosigner/common/types'; import { API } from './types'; import { getValidatedTxnWrap, getLedgerFromGenesisId, - calculateEstimatedFee, } from '../transaction/actions'; import { BaseValidatedTxnWrap } from '../transaction/baseValidatedTxnWrap'; import { ValidationResponse, ValidationStatus } from '../utils/validator'; @@ -17,22 +17,7 @@ import encryptionWrap from '../encryptionWrap'; import { Settings } from '../config'; import { extensionBrowser } from '@algosigner/common/chrome'; import { logging } from '@algosigner/common/logging'; -import { PendingTransaction } from '../../errors/transactionSign'; -import { - InvalidStructure, - InvalidFields, - InvalidMsigStructure, - NoDifferentLedgers, - MultipleTxsRequireGroup, - NonMatchingGroup, - IncompleteOrDisorderedGroup, - InvalidSigners, - TooManyTransactions, - LedgerMultipleTransactions, - SigningError, -} from '../../errors/walletTxSign'; import { buildTransaction } from '../utils/transactionBuilder'; -import { getSigningAccounts } from '../utils/multisig'; import { base64ToByteArray, byteArrayToBase64 } from '@algosigner/common/encoding'; import { removeEmptyFields } from '@algosigner/common/utils'; @@ -68,7 +53,7 @@ export class Task { // Check if there's a previous request from the same origin if (request.originTabID in Task.requests) return new Promise((resolve, reject) => { - request.error = PendingTransaction; + request.error = RequestError.PendingTransaction; reject(request); }); else Task.requests[request.originTabID] = request; @@ -218,8 +203,8 @@ export class Task { const validationErrors: Array = []; try { // We check if we're above the maximum supported group size - if (walletTransactions.length > TooManyTransactions.MAX_GROUP_SIZE) { - throw new TooManyTransactions(); + if (walletTransactions.length > RequestError.MAX_GROUP_SIZE) { + throw RequestError.TooManyTransactions; } let index = 0; @@ -240,7 +225,7 @@ export class Task { (walletTx.msig && typeof walletTx.msig !== 'object') ) { logging.log('Invalid Wallet Transaction Structure'); - throw new InvalidStructure(); + throw RequestError.InvalidStructure; } else if ( // prettier-ignore walletTx.msig && ( @@ -254,7 +239,7 @@ export class Task { ) ) { logging.log('Invalid Wallet Transaction Multisig Structure'); - throw new InvalidMsigStructure(); + throw RequestError.InvalidMsigStructure; } /** @@ -268,7 +253,7 @@ export class Task { rawTxArray[index] = rawTx; const processedTx = rawTx._getDictForDisplay(); processedTxArray[index] = processedTx; - const wrap = getValidatedTxnWrap(processedTx, processedTx['type'], false); + const wrap = getValidatedTxnWrap(processedTx, processedTx['type']); transactionWraps[index] = wrap; const genesisID = wrap.transaction.genesisID; @@ -335,7 +320,7 @@ export class Task { data = data + `Validation failed for transaction ${index} due to: ${error.message}.`; code = error.code && error.code < code ? error.code : code; }); - throw new SigningError(code, data); + throw RequestError.SigningError(code, data); } else if ( transactionWraps.some( (tx) => @@ -367,7 +352,7 @@ export class Task { ].join(', ')}]. `; }); - throw new InvalidFields(data); + throw RequestError.InvalidFields(data); } else { // Group validations const groupId = transactionWraps[0].transaction.group; @@ -378,15 +363,15 @@ export class Task { (wrap) => transactionWraps[0].transaction.genesisID === wrap.transaction.genesisID ) ) { - throw new NoDifferentLedgers(); + throw RequestError.NoDifferentLedgers; } if (!groupId) { - throw new MultipleTxsRequireGroup(); + throw RequestError.MultipleTxsRequireGroup; } if (!transactionWraps.every((wrap) => groupId === wrap.transaction.group)) { - throw new NonMatchingGroup(); + throw RequestError.NonMatchingGroup; } } else { const wrap = transactionWraps[0]; @@ -394,7 +379,7 @@ export class Task { (!wrap.msigData && wrap.signers) || (wrap.msigData && wrap.signers && !wrap.signers.length) ) { - throw new InvalidSigners(); + throw RequestError.InvalidSigners; } } @@ -408,7 +393,7 @@ export class Task { ); const recalculatedGroupID = byteArrayToBase64(recreatedGroupTxs[0].group); if (groupId !== recalculatedGroupID) { - throw new IncompleteOrDisorderedGroup(); + throw RequestError.IncompleteOrDisorderedGroup; } } @@ -857,147 +842,6 @@ export class Task { } return true; }, - // sign-allow-multisig - [JsonRpcMethod.SignAllowMultisig]: (request: any, sendResponse: Function) => { - const { passphrase, responseOriginTabID } = request.body.params; - const auth = Task.requests[responseOriginTabID]; - const message = auth.message; - - // Map the full multisig transaction here - const msig_txn = { msig: message.body.params.msig, txn: message.body.params.txn }; - - try { - // Use MainNet if specified - default to TestNet - const ledger = getLedgerFromGenesisId(msig_txn.txn.genesisID); - - // Create an encryption wrap to get the needed signing account information - const context = new encryptionWrap(passphrase); - context.unlock(async (unlockedValue: any) => { - if ('error' in unlockedValue) { - sendResponse(unlockedValue); - return false; - } - - extensionBrowser.windows.remove(auth.window_id); - - // Verify this is a multisig sign occurs in the getSigningAccounts - // This get may receive a .error in return if an appropriate account is not found - let account; - const multisigAccounts = getSigningAccounts(unlockedValue[ledger], msig_txn); - if (multisigAccounts.error) { - message.error = multisigAccounts.error.message; - } else { - // TODO: Currently we are grabbing the first non-signed account. This may change. - account = multisigAccounts.accounts[0]; - } - - if (account) { - // We can now use the found account match to get the sign key - const recoveredAccount = algosdk.mnemonicToSecretKey(account.mnemonic); - - removeEmptyFields(msig_txn.txn); - - // Modify base64 encoded fields - if ('note' in msig_txn.txn && msig_txn.txn.note !== undefined) { - msig_txn.txn.note = new Uint8Array(Buffer.from(msig_txn.txn.note)); - } - // Application transactions only - if (msig_txn.txn && msig_txn.txn.type == 'appl') { - if ('appApprovalProgram' in msig_txn.txn) { - try { - msig_txn.txn.appApprovalProgram = Uint8Array.from( - Buffer.from(msig_txn.txn.appApprovalProgram, 'base64') - ); - } catch { - message.error = - 'Error trying to parse appApprovalProgram into a Uint8Array value.'; - } - } - if ('appClearProgram' in msig_txn.txn) { - try { - msig_txn.txn.appClearProgram = Uint8Array.from( - Buffer.from(msig_txn.txn.appClearProgram, 'base64') - ); - } catch { - message.error = - 'Error trying to parse appClearProgram into a Uint8Array value.'; - } - } - if ('appArgs' in msig_txn.txn) { - try { - const tempArgs = []; - msig_txn.txn.appArgs.forEach((element) => { - tempArgs.push(Uint8Array.from(Buffer.from(element, 'base64'))); - }); - msig_txn.txn.appArgs = tempArgs; - } catch { - message.error = 'Error trying to parse appArgs into Uint8Array values.'; - } - } - } - - try { - // This step transitions a raw object into a transaction style object - const builtTx = buildTransaction(msig_txn.txn); - - // Building preimg - This allows the pks to be passed, but still use the default multisig sign with addrs - const version = msig_txn.msig.v || msig_txn.msig.version; - const threshold = msig_txn.msig.thr || msig_txn.msig.threshold; - const addrs = - msig_txn.msig.addrs || - msig_txn.msig.subsig.map((subsig) => { - return subsig.pk; - }); - const preimg = { - version: version, - threshold: threshold, - addrs: addrs, - }; - - let signedTxn; - const appendEnabled = false; // TODO: This disables append functionality until blob objects are allowed and validated. - // Check for existing signatures. Append if there are any. - if (appendEnabled && msig_txn.msig.subsig.some((subsig) => subsig.s)) { - // TODO: This should use a sent multisig blob if provided. This is a future enhancement as validation doesn't allow it currently. - // It is subject to change and is built as scaffolding for future functionality. - const encodedBlob = message.body.params.txn; - const decodedBlob = Buffer.from(encodedBlob, 'base64'); - signedTxn = algosdk.appendSignMultisigTransaction( - decodedBlob, - preimg, - recoveredAccount.sk - ); - } else { - // If this is the first signature then do a normal sign - signedTxn = algosdk.signMultisigTransaction( - builtTx, - preimg, - recoveredAccount.sk - ); - } - - // Converting the blob to an encoded string for transfer back to dApp - const b64Obj = Buffer.from(signedTxn.blob).toString('base64'); - - message.response = { - txID: signedTxn.txID, - blob: b64Obj, - }; - } catch (e) { - message.error = e.message; - } - } - // Clean class saved request - delete Task.requests[responseOriginTabID]; - MessageApi.send(message); - }); - } catch { - // On error we should remove the task - delete Task.requests[responseOriginTabID]; - return false; - } - return true; - }, // sign-allow-wallet-tx [JsonRpcMethod.SignAllowWalletTx]: (request: any, sendResponse: Function) => { const { passphrase, responseOriginTabID } = request.body.params; @@ -1142,7 +986,7 @@ export class Task { } else if (hardwareAccounts.some((a) => a === address)) { // Limit to single group transactions if (!singleGroup || transactionObjs.length > 1) { - throw new LedgerMultipleTransactions(); + throw RequestError.LedgerMultipleTransactions; } // Now that we know it is a single group adjust the transaction property to be the current wrap @@ -1191,7 +1035,7 @@ export class Task { if (!singleGroup) { data = `On group ${currentGroup}: [${data}].`; } - message.error = new SigningError(4000, data); + message.error = RequestError.SigningError(4000, data); logging.log(data); } else { signedGroups[currentGroup] = signedTxs; @@ -1308,9 +1152,6 @@ export class Task { [JsonRpcMethod.AssetsVerifiedList]: (request: any, sendResponse: Function) => { return InternalMethods[JsonRpcMethod.AssetsVerifiedList](request, sendResponse); }, - [JsonRpcMethod.AssetOptOut]: (request: any, sendResponse: Function) => { - return InternalMethods[JsonRpcMethod.AssetOptOut](request, sendResponse); - }, [JsonRpcMethod.SignSendTransaction]: (request: any, sendResponse: Function) => { return InternalMethods[JsonRpcMethod.SignSendTransaction](request, sendResponse); }, @@ -1375,48 +1216,7 @@ export class Task { return InternalMethods[JsonRpcMethod.LedgerLinkAddress](request, sendResponse); }, [JsonRpcMethod.LedgerGetSessionTxn]: (request: any, sendResponse: Function) => { - InternalMethods[JsonRpcMethod.LedgerGetSessionTxn](request, (internalResponse) => { - // V2 transactions can just pass back - if (internalResponse.transactionsOrGroups) { - sendResponse(internalResponse); - } - // V1 transactions may need to have an estimated fee - else { - let txWrap = internalResponse; - if (txWrap.transaction && txWrap.transaction.transaction) { - txWrap = txWrap.transaction; - } - - // Send response or grab params to calculate an estimated fee if there isn't one - if (txWrap.estimatedFee) { - sendResponse(txWrap); - } else { - const conn = Settings.getBackendParams( - getLedgerFromGenesisId(txWrap.transaction.genesisID), - API.Algod - ); - const sendPath = '/v2/transactions/params'; - const fetchParams: any = { - headers: { - ...conn.headers, - }, - method: 'GET', - }; - - let url = conn.url; - if (conn.port.length > 0) url += ':' + conn.port; - Task.fetchAPI(`${url}${sendPath}`, fetchParams).then((params) => { - if (txWrap.transaction.fee === params['min-fee']) { - // This object was built on front end and fee should be 0 to prevent higher fees. - txWrap.transaction.fee = 0; - } - calculateEstimatedFee(txWrap, params); - sendResponse(txWrap); - }); - } - } - }); - return true; + return InternalMethods[JsonRpcMethod.LedgerGetSessionTxn](request, sendResponse); }, [JsonRpcMethod.LedgerSendTxnResponse]: (request: any, sendResponse: Function) => { InternalMethods[JsonRpcMethod.LedgerSendTxnResponse](request, function (response) { diff --git a/packages/extension/src/background/transaction/acfgCreateTransaction.ts b/packages/extension/src/background/transaction/acfgCreateTransaction.ts index da6de50a..a9b99560 100644 --- a/packages/extension/src/background/transaction/acfgCreateTransaction.ts +++ b/packages/extension/src/background/transaction/acfgCreateTransaction.ts @@ -37,7 +37,7 @@ class AssetCreateTx implements IAssetCreateTx { /// export class AssetCreateTransaction extends BaseValidatedTxnWrap { txDerivedTypeText: string = 'Create Asset'; - constructor(params: IAssetCreateTx, v1Validations: boolean) { - super(params, AssetCreateTx, v1Validations); + constructor(params: IAssetCreateTx) { + super(params, AssetCreateTx); } } diff --git a/packages/extension/src/background/transaction/acfgDestroyTransaction.ts b/packages/extension/src/background/transaction/acfgDestroyTransaction.ts index c20f5df5..bdbed06a 100644 --- a/packages/extension/src/background/transaction/acfgDestroyTransaction.ts +++ b/packages/extension/src/background/transaction/acfgDestroyTransaction.ts @@ -27,7 +27,7 @@ class AssetDestroyTx implements IAssetDestroyTx { /// export class AssetDestroyTransaction extends BaseValidatedTxnWrap { txDerivedTypeText: string = 'Destroy Asset'; - constructor(params: IAssetDestroyTx, v1Validations: boolean) { - super(params, AssetDestroyTx, v1Validations); + constructor(params: IAssetDestroyTx) { + super(params, AssetDestroyTx); } } diff --git a/packages/extension/src/background/transaction/acfgTransaction.ts b/packages/extension/src/background/transaction/acfgTransaction.ts index c2feb3c1..90c293cd 100644 --- a/packages/extension/src/background/transaction/acfgTransaction.ts +++ b/packages/extension/src/background/transaction/acfgTransaction.ts @@ -55,7 +55,7 @@ const requiredAcfgParams: Array = [ /// export class AssetConfigTransaction extends BaseValidatedTxnWrap { txDerivedTypeText: string = 'Modify Asset'; - constructor(params: IAssetConfigTx, v1Validations: boolean) { - super(params, AssetConfigTx, v1Validations, requiredAcfgParams); + constructor(params: IAssetConfigTx) { + super(params, AssetConfigTx, requiredAcfgParams); } } diff --git a/packages/extension/src/background/transaction/actions.ts b/packages/extension/src/background/transaction/actions.ts index f413739f..67e6bff0 100644 --- a/packages/extension/src/background/transaction/actions.ts +++ b/packages/extension/src/background/transaction/actions.ts @@ -21,7 +21,7 @@ import { AssetClawbackTransaction } from './axferClawbackTransaction'; import { KeyregTransaction } from './keyregTransaction'; import { ApplTransaction } from './applTransaction'; import { TransactionType } from '@algosigner/common/types/transaction'; -import { RequestError } from '@algosigner/common/types'; +import { RequestError } from '@algosigner/common/errors'; import { BaseValidatedTxnWrap } from './baseValidatedTxnWrap'; import { Settings } from '../config'; import { getBaseSupportedLedgers } from '@algosigner/common/types/ledgers'; @@ -34,7 +34,6 @@ import algosdk from 'algosdk'; export function getValidatedTxnWrap( txn: object, type: string, - v1Validations: boolean = true ): BaseValidatedTxnWrap { let validatedTxnWrap: BaseValidatedTxnWrap = undefined; let error: RequestError = undefined; @@ -44,19 +43,19 @@ export function getValidatedTxnWrap( switch (type.toLowerCase()) { case TransactionType.Pay: - validatedTxnWrap = new PayTransaction(txn as IPaymentTx, v1Validations); + validatedTxnWrap = new PayTransaction(txn as IPaymentTx); break; case TransactionType.Acfg: // Validate any of the 3 types of transactions that can occur with acfg // Use the first error as the passback error. try { - validatedTxnWrap = new AssetConfigTransaction(txn as IAssetConfigTx, v1Validations); + validatedTxnWrap = new AssetConfigTransaction(txn as IAssetConfigTx); } catch (e) { error = e; } if (!validatedTxnWrap) { try { - validatedTxnWrap = new AssetCreateTransaction(txn as IAssetCreateTx, v1Validations); + validatedTxnWrap = new AssetCreateTransaction(txn as IAssetCreateTx); } catch (e) { e.data = [error.data, e.data].join(' '); error = e; @@ -64,7 +63,7 @@ export function getValidatedTxnWrap( } if (!validatedTxnWrap) { try { - validatedTxnWrap = new AssetDestroyTransaction(txn as IAssetDestroyTx, v1Validations); + validatedTxnWrap = new AssetDestroyTransaction(txn as IAssetDestroyTx); } catch (e) { e.data = [error.data, e.data].join(' '); error = e; @@ -75,19 +74,19 @@ export function getValidatedTxnWrap( } break; case TransactionType.Afrz: - validatedTxnWrap = new AssetFreezeTransaction(txn as IAssetFreezeTx, v1Validations); + validatedTxnWrap = new AssetFreezeTransaction(txn as IAssetFreezeTx); break; case TransactionType.Axfer: // Validate any of the 4 types of transactions that can occur with axfer // Use the first error as the passback error. try { - validatedTxnWrap = new AssetAcceptTransaction(txn as IAssetAcceptTx, v1Validations); + validatedTxnWrap = new AssetAcceptTransaction(txn as IAssetAcceptTx); } catch (e) { error = e; } if (!validatedTxnWrap) { try { - validatedTxnWrap = new AssetCloseTransaction(txn as IAssetCloseTx, v1Validations); + validatedTxnWrap = new AssetCloseTransaction(txn as IAssetCloseTx); } catch (e) { e.message = [error.message, e.message].join(' '); error = e; @@ -95,7 +94,7 @@ export function getValidatedTxnWrap( } if (!validatedTxnWrap) { try { - validatedTxnWrap = new AssetTransferTransaction(txn as IAssetTransferTx, v1Validations); + validatedTxnWrap = new AssetTransferTransaction(txn as IAssetTransferTx); } catch (e) { e.message = [error.message, e.message].join(' '); error = e; @@ -103,7 +102,7 @@ export function getValidatedTxnWrap( } if (!validatedTxnWrap) { try { - validatedTxnWrap = new AssetClawbackTransaction(txn as IAssetClawbackTx, v1Validations); + validatedTxnWrap = new AssetClawbackTransaction(txn as IAssetClawbackTx); } catch (e) { e.message = [error.message, e.message].join(' '); error = e; @@ -114,10 +113,10 @@ export function getValidatedTxnWrap( } break; case TransactionType.Keyreg: - validatedTxnWrap = new KeyregTransaction(txn as IKeyRegistrationTx, v1Validations); + validatedTxnWrap = new KeyregTransaction(txn as IKeyRegistrationTx); break; case TransactionType.Appl: - validatedTxnWrap = new ApplTransaction(txn as IApplTx, v1Validations); + validatedTxnWrap = new ApplTransaction(txn as IApplTx); break; default: throw new Error('Type of transaction not specified or known.'); diff --git a/packages/extension/src/background/transaction/afrzTransaction.ts b/packages/extension/src/background/transaction/afrzTransaction.ts index cffef463..664c6dd8 100644 --- a/packages/extension/src/background/transaction/afrzTransaction.ts +++ b/packages/extension/src/background/transaction/afrzTransaction.ts @@ -29,7 +29,7 @@ class AssetFreezeTx implements IAssetFreezeTx { /// export class AssetFreezeTransaction extends BaseValidatedTxnWrap { txDerivedTypeText: string = 'Freeze Asset'; - constructor(params: IAssetFreezeTx, v1Validations: boolean) { - super(params, AssetFreezeTx, v1Validations); + constructor(params: IAssetFreezeTx) { + super(params, AssetFreezeTx); } } diff --git a/packages/extension/src/background/transaction/applTransaction.ts b/packages/extension/src/background/transaction/applTransaction.ts index 78548eca..52a5da5b 100644 --- a/packages/extension/src/background/transaction/applTransaction.ts +++ b/packages/extension/src/background/transaction/applTransaction.ts @@ -40,7 +40,7 @@ export class ApplTx implements IApplTx { /// export class ApplTransaction extends BaseValidatedTxnWrap { txDerivedTypeText: string = 'Application'; - constructor(params: IApplTx, v1Validations: boolean) { - super(params, ApplTx, v1Validations); + constructor(params: IApplTx) { + super(params, ApplTx); } } diff --git a/packages/extension/src/background/transaction/axferAcceptTransaction.ts b/packages/extension/src/background/transaction/axferAcceptTransaction.ts index fc3ac0ee..554fdc39 100644 --- a/packages/extension/src/background/transaction/axferAcceptTransaction.ts +++ b/packages/extension/src/background/transaction/axferAcceptTransaction.ts @@ -1,6 +1,6 @@ +import { RequestError } from '@algosigner/common/errors'; import { IAssetAcceptTx } from '@algosigner/common/interfaces/axfer_accept'; import { BaseValidatedTxnWrap } from './baseValidatedTxnWrap'; -import { InvalidTransactionStructure } from '../../errors/validation'; /// // Base implementation of the transactions type interface, for use in the export wrapper class below. @@ -30,14 +30,14 @@ class AssetAcceptTx implements IAssetAcceptTx { /// export class AssetAcceptTransaction extends BaseValidatedTxnWrap { txDerivedTypeText: string = 'Asset Opt-In'; - constructor(params: IAssetAcceptTx, v1Validations: boolean) { - super(params, AssetAcceptTx, v1Validations); + constructor(params: IAssetAcceptTx) { + super(params, AssetAcceptTx); // Additional check to verify that amount is 0 and address from and to are the same if (this.transaction && this.transaction['amount'] && this.transaction['amount'] != 0) { - throw new InvalidTransactionStructure(`Creation of AssetAcceptTx has an invalid amount.`); + throw RequestError.InvalidTransactionStructure(`Creation of AssetAcceptTx has an invalid amount.`); } if (this.transaction && this.transaction['to'] !== this.transaction['from']) { - throw new InvalidTransactionStructure( + throw RequestError.InvalidTransactionStructure( `Creation of AssetAcceptTx has non identical to and from fields.` ); } diff --git a/packages/extension/src/background/transaction/axferClawbackTransaction.ts b/packages/extension/src/background/transaction/axferClawbackTransaction.ts index 005a7748..776e19f0 100644 --- a/packages/extension/src/background/transaction/axferClawbackTransaction.ts +++ b/packages/extension/src/background/transaction/axferClawbackTransaction.ts @@ -31,7 +31,7 @@ class AssetClawbackTx implements IAssetClawbackTx { /// export class AssetClawbackTransaction extends BaseValidatedTxnWrap { txDerivedTypeText: string = 'Asset Clawback'; - constructor(params: IAssetClawbackTx, v1Validations: boolean) { - super(params, AssetClawbackTx, v1Validations); + constructor(params: IAssetClawbackTx) { + super(params, AssetClawbackTx); } } diff --git a/packages/extension/src/background/transaction/axferCloseTransaction.ts b/packages/extension/src/background/transaction/axferCloseTransaction.ts index 6ca40c03..43236fa4 100644 --- a/packages/extension/src/background/transaction/axferCloseTransaction.ts +++ b/packages/extension/src/background/transaction/axferCloseTransaction.ts @@ -1,6 +1,6 @@ +import { RequestError } from '@algosigner/common/errors'; import { IAssetCloseTx } from '@algosigner/common/interfaces/axfer_close'; import { BaseValidatedTxnWrap } from './baseValidatedTxnWrap'; -import { InvalidTransactionStructure } from '../../errors/validation'; /// // Base implementation of the transactions type interface, for use in the export wrapper class below. @@ -31,11 +31,11 @@ class AssetCloseTx implements IAssetCloseTx { /// export class AssetCloseTransaction extends BaseValidatedTxnWrap { txDerivedTypeText: string = 'Asset Opt-Out'; - constructor(params: IAssetCloseTx, v1Validations: boolean) { - super(params, AssetCloseTx, v1Validations); + constructor(params: IAssetCloseTx) { + super(params, AssetCloseTx); // Additional check to verify that address from and to are the same if (this.transaction && this.transaction['to'] !== this.transaction['from']) { - throw new InvalidTransactionStructure( + throw RequestError.InvalidTransactionStructure( `Creation of AssetCloseTx has non identical to and from fields.` ); } diff --git a/packages/extension/src/background/transaction/axferTransaction.ts b/packages/extension/src/background/transaction/axferTransaction.ts index d39bc8d4..8c9e3f88 100644 --- a/packages/extension/src/background/transaction/axferTransaction.ts +++ b/packages/extension/src/background/transaction/axferTransaction.ts @@ -30,7 +30,7 @@ class AssetTransferTx implements IAssetTransferTx { /// export class AssetTransferTransaction extends BaseValidatedTxnWrap { txDerivedTypeText: string = 'Asset Transfer'; - constructor(params: IAssetTransferTx, v1Validations: boolean) { - super(params, AssetTransferTx, v1Validations); + constructor(params: IAssetTransferTx) { + super(params, AssetTransferTx); } } diff --git a/packages/extension/src/background/transaction/baseValidatedTxnWrap.ts b/packages/extension/src/background/transaction/baseValidatedTxnWrap.ts index ed72f264..6abb15da 100644 --- a/packages/extension/src/background/transaction/baseValidatedTxnWrap.ts +++ b/packages/extension/src/background/transaction/baseValidatedTxnWrap.ts @@ -1,8 +1,8 @@ import algosdk from 'algosdk'; import { WalletMultisigMetadata } from '@algosigner/common/types'; -import { Validate, ValidationResponse, ValidationStatus } from '../utils/validator'; +import { RequestError } from '@algosigner/common/errors'; +import { Validate, ValidationResponse } from '../utils/validator'; import { logging } from '@algosigner/common/logging'; -import { InvalidTransactionStructure } from '../../errors/validation'; type AssetInfo = { unitName: string; @@ -10,7 +10,6 @@ type AssetInfo = { }; const BIGINT_FIELDS = ['amount', 'assetTotal']; -const V1_WARNING = `This signing method is going to be deprecated in favor of the more secure 'AlgoSigner.signTxn()' method.`; // // Base validated transaction wrap @@ -25,12 +24,7 @@ export class BaseValidatedTxnWrap { signers: Array; authAddr: string; - constructor( - params: any, - txnType: any, - v1Validations: boolean = true, - requiredParamsSet: Array = undefined - ) { + constructor(params: any, txnType: any, requiredParamsSet: Array = undefined) { this.transaction = new txnType(); const missingFields = []; const extraFields = []; @@ -59,7 +53,7 @@ export class BaseValidatedTxnWrap { // Throwing error here so that missing fields can be combined. if (missingFields.length > 0) { - throw new InvalidTransactionStructure( + throw RequestError.InvalidTransactionStructure( `Creation of ${ txnType.name } has missing or invalid required properties: ${missingFields.toString()}.` @@ -78,24 +72,23 @@ export class BaseValidatedTxnWrap { // Done more liberally on v2 since we use the unmodified transaction afterwards if ( // First we check for UintArrays and make them readable - (prop === 'group' || - prop === 'appApprovalProgram' || - prop === 'appClearProgram' || - prop === 'assetMetadataHash' || - prop === 'lease' || - prop === 'selectionKey' || - prop === 'stateProofKey' || - prop === 'voteKey') && - !v1Validations + prop === 'group' || + prop === 'appApprovalProgram' || + prop === 'appClearProgram' || + prop === 'assetMetadataHash' || + prop === 'lease' || + prop === 'selectionKey' || + prop === 'stateProofKey' || + prop === 'voteKey' ) { this.transaction[prop] = Buffer.from(params[prop]).toString('base64'); // Then we check for UintArray arrays - } else if (prop === 'appArgs' && !v1Validations) { + } else if (prop === 'appArgs') { this.transaction[prop] = this.transaction[prop].map((arg) => Buffer.from(arg).toString('base64') ); // Then for address arrays - } else if (prop === 'appAccounts' && !v1Validations) { + } else if (prop === 'appAccounts') { const accArray = params[prop]; if (Array.isArray(accArray) && accArray.every((accObj) => 'publicKey' in accObj)) { this.transaction[prop] = accArray.map((a) => algosdk.encodeAddress(a.publicKey)); @@ -115,32 +108,9 @@ export class BaseValidatedTxnWrap { // Throwing error here so that extra fields can be combined. if (extraFields.length > 0) { - throw new InvalidTransactionStructure( + throw RequestError.InvalidTransactionStructure( `Creation of ${txnType.name} has extra or invalid fields: ${extraFields.toString()}.` ); } - - if (v1Validations) { - // We mark all v1 transactions as soon to be deprecated - this.validityObject['Deprecated'] = new ValidationResponse({ - status: ValidationStatus.Dangerous, - info: V1_WARNING, - }); - - // We mark atomic transactions as an invalid field for v1 - if (params['group']) { - this.validityObject['group'] = new ValidationResponse({ - status: ValidationStatus.Invalid, - }); - } - - // If we don't have a flatFee or it is falsy and we have a non-zero fee, create a warning. - if (!params['flatFee'] && params['fee'] && params['fee'] > 0) { - this.validityObject['flatFee'] = new ValidationResponse({ - status: ValidationStatus.Warning, - info: 'The fee is subject to change without flatFee enabled.', - }); - } - } } } diff --git a/packages/extension/src/background/transaction/keyregTransaction.ts b/packages/extension/src/background/transaction/keyregTransaction.ts index 23763d83..855c9bbd 100644 --- a/packages/extension/src/background/transaction/keyregTransaction.ts +++ b/packages/extension/src/background/transaction/keyregTransaction.ts @@ -31,7 +31,7 @@ class KeyRegistrationTx implements IKeyRegistrationTx { /// export class KeyregTransaction extends BaseValidatedTxnWrap { txDerivedTypeText: string = 'Key Registration'; - constructor(params: IKeyRegistrationTx, v1Validations: boolean) { - super(params, KeyRegistrationTx, v1Validations); + constructor(params: IKeyRegistrationTx) { + super(params, KeyRegistrationTx); } } diff --git a/packages/extension/src/background/transaction/payTransaction.ts b/packages/extension/src/background/transaction/payTransaction.ts index 7fbd0937..d0034386 100644 --- a/packages/extension/src/background/transaction/payTransaction.ts +++ b/packages/extension/src/background/transaction/payTransaction.ts @@ -29,7 +29,7 @@ class PaymentTx implements IPaymentTx { /// export class PayTransaction extends BaseValidatedTxnWrap { txDerivedTypeText: string = 'Pay Algos'; - constructor(params: IPaymentTx, v1Validations: boolean) { - super(params, PaymentTx, v1Validations); + constructor(params: IPaymentTx) { + super(params, PaymentTx); } } diff --git a/packages/extension/src/background/utils/multisig.ts b/packages/extension/src/background/utils/multisig.ts deleted file mode 100644 index 6d910694..00000000 --- a/packages/extension/src/background/utils/multisig.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-types */ -import logging from '@algosigner/common/logging'; -import { - MultisigNoMatch, - MultisigAlreadySigned, - MultisigInvalidMsig, -} from '../../errors/transactionSign'; - -/// -// Checks if the provided transaction is a multisig transaction -/// -export function isMultisig(transaction): boolean { - if ( - transaction.msig && - ((transaction.msig.subsig && transaction.msig.v && transaction.msig.thr) || - (transaction.msig.addrs && transaction.msig.version && transaction.msig.threshold)) - ) { - // TODO: Need to have a check here for various multisig validity concerns including bad from hash - return true; - } - return false; -} - -/// -// Attempt to find an AlgoSign address to sign with in the transaction msig component. -/// -export function getSigningAccounts( - ledgerAddresses: any, - transaction: any -): { - accounts?: Array; - error?: MultisigInvalidMsig | MultisigAlreadySigned | MultisigNoMatch; -} { - var foundAccount = false; - const multisigAccounts = { accounts: [], error: undefined }; - - // Verify that this transaction includes multisig components - if (!isMultisig(transaction)) { - multisigAccounts.error = new MultisigInvalidMsig('Multisig parameters are invalid.'); - // Return immediately - No need to continue if this is the case as the msig component is required - return multisigAccounts; - } - - // Cycle the msig accounts to match addresses to those in AlgoSigner - const subsig_addrs = transaction.msig.subsig || transaction.msig.addrs; - try { - if (subsig_addrs && subsig_addrs.length > 0) { - subsig_addrs.forEach((account) => { - for (var i = ledgerAddresses.length - 1; i >= 0; i--) { - if (ledgerAddresses[i].address === account || ledgerAddresses[i].address === account.pk) { - // We found an account so indicate the value as true, used in the case that the only found accounts are already signed - foundAccount = true; - if (!account.s) { - // We found an unsigned account that we own, add it to the list of accounts we can now sign - multisigAccounts.accounts.push(ledgerAddresses[i]); - } - } - } - }); - } - } catch (e) { - logging.log(e); - } - - // If we didn't find an account to return then return an error. - // This error depends on if we found the address at all or not. - if (multisigAccounts.accounts.length === 0) { - if (foundAccount) { - multisigAccounts.error = new MultisigAlreadySigned('Matching addresses have already signed.'); - } else { - multisigAccounts.error = new MultisigNoMatch('No address match found.'); - } - } - - return multisigAccounts; -} diff --git a/packages/extension/src/errors/transactionSign.ts b/packages/extension/src/errors/transactionSign.ts deleted file mode 100644 index e8b41fcd..00000000 --- a/packages/extension/src/errors/transactionSign.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { RequestError } from '@algosigner/common/types'; - -export class NoAccountMatch extends RequestError { - constructor(address: string, ledger: string) { - super( - `No matching account found on AlgoSigner for address: "${address}" on network ${ledger}.`, - 4100, - 'NoAccountMatch' - ); - Error.captureStackTrace(this, NoAccountMatch); - } -} - -export class MultisigNoMatch extends Error { - constructor(message?: any) { - message ? super(message) : super(); - this.name = 'NoMultisigMatch'; - Error.captureStackTrace(this, MultisigNoMatch); - } -} - -export class MultisigAlreadySigned extends Error { - constructor(message?: any) { - message ? super(message) : super(); - this.name = 'MultisigAlreadySigned'; - Error.captureStackTrace(this, MultisigAlreadySigned); - } -} - -export class MultisigInvalidMsig extends Error { - constructor(message?: any) { - message ? super(message) : super(); - this.name = 'MultisigInvalidMsig'; - Error.captureStackTrace(this, MultisigInvalidMsig); - } -} - -export class PendingTransaction extends RequestError { - constructor() { - super('Another query processing', 4200, 'PendingTransaction'); - Error.captureStackTrace(this, PendingTransaction); - } -} diff --git a/packages/extension/src/errors/validation.test.ts b/packages/extension/src/errors/validation.test.ts deleted file mode 100644 index 9c3f0491..00000000 --- a/packages/extension/src/errors/validation.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { InvalidTransactionStructure } from './validation'; - -test('Invalid transaction', () => { - function throwError () { - throw new InvalidTransactionStructure("Test Message"); - } - expect(() => throwError()).toThrow(InvalidTransactionStructure); -}); \ No newline at end of file diff --git a/packages/extension/src/errors/validation.ts b/packages/extension/src/errors/validation.ts deleted file mode 100644 index 95b77742..00000000 --- a/packages/extension/src/errors/validation.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { RequestError } from '@algosigner/common/types'; - -export class InvalidTransactionStructure extends RequestError { - constructor(data?: any) { - super( - 'Validation failed for transaction due to invalid structure.', - 4300, - 'InvalidTransactionStructure', - data - ); - Error.captureStackTrace(this, InvalidTransactionStructure); - } -} - -export class InvalidFields extends RequestError { - constructor(data?: any) { - super( - 'Validation failed for transaction due to invalid properties.', - 4300, - 'InvalidFields', - data - ); - Error.captureStackTrace(this, InvalidFields); - } -} diff --git a/packages/extension/src/errors/walletTxSign.ts b/packages/extension/src/errors/walletTxSign.ts deleted file mode 100644 index 3ae46c9a..00000000 --- a/packages/extension/src/errors/walletTxSign.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { RequestError } from '@algosigner/common/types'; - -export class InvalidStructure extends RequestError { - constructor() { - super( - "The provided transaction object doesn't adhere to the correct structure.", - 4300, - 'InvalidStructure' - ); - Error.captureStackTrace(this, InvalidStructure); - } -} - -export class InvalidFields extends RequestError { - constructor(data?: any) { - super( - 'Validation failed for transaction due to invalid properties.', - 4300, - 'InvalidFields', - data - ); - Error.captureStackTrace(this, InvalidFields); - } -} - -export class InvalidMsigStructure extends RequestError { - constructor() { - super( - "The provided multisig data doesn't adhere to the correct structure.", - 4300, - 'InvalidMsigStructure' - ); - Error.captureStackTrace(this, InvalidMsigStructure); - } -} - -export class NoDifferentLedgers extends RequestError { - constructor() { - super('All transactions need to belong to the same ledger.', 4300, 'NoDifferentLedgers'); - Error.captureStackTrace(this, NoDifferentLedgers); - } -} - -export class MultipleTxsRequireGroup extends RequestError { - constructor() { - super( - 'If signing multiple transactions, they need to belong to a same group.', - 4200, - 'MultipleTxsRequireGroup' - ); - Error.captureStackTrace(this, MultipleTxsRequireGroup); - } -} - -export class NonMatchingGroup extends RequestError { - constructor() { - super('All transactions need to belong to the same group.', 4300, 'NonMatchingGroup'); - Error.captureStackTrace(this, NonMatchingGroup); - } -} - -export class IncompleteOrDisorderedGroup extends RequestError { - constructor() { - super( - 'The transaction group is incomplete or presented in a different order than when it was created.', - 4300, - 'IncompleteOrDisorderedGroup' - ); - Error.captureStackTrace(this, IncompleteOrDisorderedGroup); - } -} - -export class InvalidSigners extends RequestError { - constructor() { - super( - 'Signers array should only be provided for multisigs (at least one signer) or for reference-only transactions belonging to a group (empty array).', - 4300, - 'InvalidSigners' - ); - Error.captureStackTrace(this, InvalidSigners); - } -} - -export class TooManyTransactions extends RequestError { - // Maximum amount of transactions supported on a single group - static MAX_GROUP_SIZE = 16; - constructor() { - super( - `The ledger does not support signing more than ${TooManyTransactions.MAX_GROUP_SIZE} transactions at a time.`, - 4201, - 'TooManyTransactions' - ); - Error.captureStackTrace(this, TooManyTransactions); - } -} - -export class LedgerMultipleTransactions extends RequestError { - constructor() { - super( - 'Ledger hardware device signing not available for multiple transactions.', - 4201, - 'LedgerMultipleTransactions' - ); - Error.captureStackTrace(this, LedgerMultipleTransactions); - } -} - -export class SigningError extends RequestError { - constructor(code: number, data?: any) { - super( - 'There was a problem signing the transaction(s).', - code, - 'SigningError', - data - ); - Error.captureStackTrace(this, SigningError); - } -} \ No newline at end of file diff --git a/packages/test-project/tests/dapp-signtxn.test.js b/packages/test-project/tests/dapp-signtxn.test.js index 060c4aaf..81f7fc38 100644 --- a/packages/test-project/tests/dapp-signtxn.test.js +++ b/packages/test-project/tests/dapp-signtxn.test.js @@ -154,7 +154,7 @@ describe('Error Use cases', () => { ).resolves.toMatchObject({ message: expect.stringContaining('There was a problem signing the transaction(s).'), code: 4100, - name: expect.stringContaining('SigningError'), + name: expect.stringContaining('AlgoSignerRequestError'), data: expect.stringContaining(accounts.ui.address), }); }); @@ -187,7 +187,7 @@ describe('Error Use cases', () => { ).resolves.toMatchObject({ message: expect.stringContaining('Signers array should only'), code: 4300, - name: expect.stringContaining('InvalidSigners'), + name: expect.stringContaining('AlgoSignerRequestError'), }); }); @@ -277,7 +277,7 @@ describe('Group Transactions Use cases', () => { ).resolves.toMatchObject({ message: expect.stringContaining('group is incomplete'), code: 4300, - name: expect.stringContaining('IncompleteOrDisorderedGroup'), + name: expect.stringContaining('AlgoSignerRequestError'), }); }); @@ -362,7 +362,7 @@ describe('Group Transactions Use cases', () => { ).resolves.toMatchObject({ message: expect.stringContaining('16 transactions at a time'), code: 4201, - name: expect.stringContaining('TooManyTransactions'), + name: expect.stringContaining('AlgoSignerRequestError'), }); }); diff --git a/packages/ui/src/components/Account/AssetsList.ts b/packages/ui/src/components/Account/AssetsList.ts index 5dd322e6..5321cba4 100644 --- a/packages/ui/src/components/Account/AssetsList.ts +++ b/packages/ui/src/components/Account/AssetsList.ts @@ -60,9 +60,16 @@ const AssetsList: FunctionalComponent = (props: any) => { ledger: ledger, passphrase: pwd, address: address, - id: showAsset['asset-id'], + txnParams: { + type: 'axfer', + assetIndex: showAsset['asset-id'], + amount: 0, + from: address, + to: address, + closeRemainderTo: address, + }, }; - sendMessage(JsonRpcMethod.AssetOptOut, params, function (response) { + sendMessage(JsonRpcMethod.SignSendTransaction, params, function (response) { setLoading(false); if ('error' in response) { switch (response.error) { diff --git a/packages/ui/src/components/LedgerDevice/LedgerHardwareSign.ts b/packages/ui/src/components/LedgerDevice/LedgerHardwareSign.ts index 2bd9e26c..f1073fb6 100644 --- a/packages/ui/src/components/LedgerDevice/LedgerHardwareSign.ts +++ b/packages/ui/src/components/LedgerDevice/LedgerHardwareSign.ts @@ -27,46 +27,44 @@ const LedgerHardwareSign: FunctionalComponent = () => { const [sessionTxnObj, setSessionTxnObj] = useState({}); useEffect(() => { - if (txn.transaction === undefined && error === '') { - try { - sendMessage(JsonRpcMethod.LedgerGetSessionTxn, {}, function (response) { - if (response.error) { - setError(response.error); + try { + sendMessage(JsonRpcMethod.LedgerGetSessionTxn, {}, function (response) { + if (response.error) { + setError(response.error); + } else { + // Get the single transaction to sign and put it in the same format + let primaryTx; + if (response.transactionWraps && response.transactionWraps.length > 0) { + primaryTx = response.transactionWraps[0]; } else { - // Get the single transaction to sign and put it in the same format - let primaryTx; - if (response.transactionWraps && response.transactionWraps.length > 0) { - primaryTx = response.transactionWraps[0]; - } else { - primaryTx = { - transaction: response.transaction, - estimatedFee: response.estimatedFee, - txDerivedTypeText: response.txDerivedTypeText, - }; - } + primaryTx = { + transaction: response.transaction, + estimatedFee: response.estimatedFee, + txDerivedTypeText: response.txDerivedTypeText, + }; + } - getBaseSupportedLedgers().forEach((l) => { - if (primaryTx.transaction?.genesisID === l['genesisId']) { - setLedger(l['name']); + getBaseSupportedLedgers().forEach((l) => { + if (primaryTx.transaction?.genesisID === l['genesisId']) { + setLedger(l['name']); - // Update the ledger dropdown to the signing one - sendMessage(JsonRpcMethod.ChangeLedger, { ledger: l['name'] }, function () { - store.setLedger(l['name']); - }); - } - }); + // Update the ledger dropdown to the signing one + sendMessage(JsonRpcMethod.ChangeLedger, { ledger: l['name'] }, function () { + store.setLedger(l['name']); + }); + } + }); - // Update account value to the signer - setAccount(primaryTx.transaction?.from); + // Update account value to the signer + setAccount(primaryTx.transaction?.from); - setSessionTxnObj(response); - setTxn(primaryTx); - } - }); - } catch (ex) { - setError('Error retrieving transaction from AlgoSigner.'); - logging.log(`${JSON.stringify(ex)}`, 2); - } + setSessionTxnObj(response); + setTxn(primaryTx); + } + }); + } catch (ex) { + setError('Error retrieving transaction from AlgoSigner.'); + logging.log(`${JSON.stringify(ex)}`, 2); } }, []); @@ -114,9 +112,9 @@ const LedgerHardwareSign: FunctionalComponent = () => { ${isComplete && html`
-

${txResponseHeader}

-

${txResponseDetail}

-

You may now close this site and relaunch AlgoSigner.

+

${txResponseHeader}

+

${txResponseDetail}

+

You may now close this site and relaunch AlgoSigner.

`} ${isComplete === false && diff --git a/packages/ui/src/components/LedgerDevice/structure/ledgerActions.ts b/packages/ui/src/components/LedgerDevice/structure/ledgerActions.ts index bcc75814..e12d38f0 100644 --- a/packages/ui/src/components/LedgerDevice/structure/ledgerActions.ts +++ b/packages/ui/src/components/LedgerDevice/structure/ledgerActions.ts @@ -163,7 +163,7 @@ const getAllAddresses = async (): Promise => { /// function cleanseBuildEncodeUnsignedTransaction(transaction: any): any { // If there's no dApp structure, we're coming from the UI - if (!transaction.encodedTxn && !transaction.groupsToSign) { + if (!transaction.groupsToSign) { const removedFieldsTxn = removeEmptyFields(transaction.transaction); // Explicit conversion of amount. Ledger transactions are stringified and retrieved, @@ -185,18 +185,6 @@ function cleanseBuildEncodeUnsignedTransaction(transaction: any): any { return { transaction: byteTxn, error: '' }; } - // If coming from a v1 sign we will have an encodedTxn object - if (transaction.encodedTxn) { - const byteTxn = new Uint8Array( - Buffer.from(transaction.encodedTxn, 'base64') - .toString('binary') - .split('') - .map((x) => x.charCodeAt(0)) - ); - - return { transaction: byteTxn, error: '' }; - } - // Using ledgerGroup if provided since the user may sign multiple more by the time we sign. // Defaulting to current after, but making sure we don't go above the current length. const { groupsToSign, currentGroup, ledgerGroup } = transaction; @@ -214,7 +202,6 @@ function cleanseBuildEncodeUnsignedTransaction(transaction: any): any { return byteWalletTxn; }); - // If we didn't have a v1 txn then verify we have some transactionObjs if (transactionObjs.length === 0) { return { transaction: undefined, @@ -224,9 +211,9 @@ function cleanseBuildEncodeUnsignedTransaction(transaction: any): any { // Currently we only allow a single transaction going into Ledger. // TODO: To work with groups in the future this should grab the first acceptable one, not the first one overall. - const encodedTxn = transactionObjs[0]; + const txToSign = transactionObjs[0]; - return { transaction: encodedTxn, error: '' }; + return { transaction: txToSign, error: '' }; } const getAddress = async (): Promise => { @@ -284,7 +271,7 @@ const signTransaction = async (txn: any): Promise => { return lar; } - // Sign method accesps a message that is "hex" format, need to convert + // Sign method accepts a message that is "hex" format, need to convert // and remove any empty fields before the conversion const txnResponse = cleanseBuildEncodeUnsignedTransaction(txn); const decodedTxn = algosdk.decodeUnsignedTransaction(txnResponse.transaction); diff --git a/packages/ui/src/pages/ImportAccount.ts b/packages/ui/src/pages/ImportAccount.ts index 19200889..b553dfec 100644 --- a/packages/ui/src/pages/ImportAccount.ts +++ b/packages/ui/src/pages/ImportAccount.ts @@ -97,7 +97,7 @@ const ImportAccount: FunctionalComponent = (props: any) => { setMnemonicArray(localMnemonicArray); }; - const handleWordInput = (e) => { + const handleWordJumps = (e) => { const key = (e as KeyboardEvent).key; const separators = [' ', ',', '.']; @@ -113,6 +113,12 @@ const ImportAccount: FunctionalComponent = (props: any) => { } }; + const handleWordInput = (e, index) => { + const localMnemonicArray = [...mnemonicArray]; + localMnemonicArray[index] = e.target.value; + setMnemonicArray(localMnemonicArray); + }; + const handleAddressInput = (e) => { setAddress(e.target.value); }; @@ -198,7 +204,8 @@ const ImportAccount: FunctionalComponent = (props: any) => { id="${`mnemonicWord${index}`}" type="password" onPaste=${handleMnemonicPaste} - onKeyDown=${handleWordInput} + onKeyDown=${handleWordJumps} + onInput=${(e) => handleWordInput(e, index)} value=${mnemonicArray[index]} />