From 856ac04c233a9768bfb59ca1a20f416f7a95f4cd Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Wed, 28 Jul 2021 10:59:34 -0700 Subject: [PATCH] Add scenarios for non-atomic groups --- src/App.tsx | 235 +++++++++++++++++++++++++++--------------- src/scenarios.ts | 261 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 366 insertions(+), 130 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8713a85..54013b7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -126,19 +126,29 @@ const STestButton = styled(Button as any)` margin: 12px; `; +interface IResult { + method: string; + body: Array< + Array<{ + txID: string; + signingAddress?: string; + signature: string; + } | null> + >; +} + interface IAppState { connector: WalletConnect | null; fetching: boolean; connected: boolean; showModal: boolean; pendingRequest: boolean; - signedTxns: Uint8Array[] | null; - pendingSubmission: boolean | number; - pendingSubmissionError: Error | null; + signedTxns: Uint8Array[][] | null; + pendingSubmissions: Array; uri: string; accounts: string[]; address: string; - result: any | null; + result: IResult | null; chain: ChainType; assets: IAssetData[]; } @@ -150,8 +160,7 @@ const INITIAL_STATE: IAppState = { showModal: false, pendingRequest: false, signedTxns: null, - pendingSubmission: false, - pendingSubmissionError: null, + pendingSubmissions: [], uri: "", accounts: [], address: "", @@ -160,7 +169,7 @@ const INITIAL_STATE: IAppState = { assets: [], }; -class App extends React.Component { +class App extends React.Component { public state: IAppState = { ...INITIAL_STATE, }; @@ -289,8 +298,7 @@ class App extends React.Component { public toggleModal = () => this.setState({ showModal: !this.state.showModal, - pendingSubmission: false, - pendingSubmissionError: null, + pendingSubmissions: [], }); public signTxnScenario = async (scenario: Scenario) => { @@ -309,78 +317,107 @@ class App extends React.Component { // toggle pending request indicator this.setState({ pendingRequest: true }); - const walletTxns: IWalletTransaction[] = txnsToSign.map(({ txn, shouldSign, authAddr }) => ({ - txn: Buffer.from(algosdk.encodeUnsignedTransaction(txn)).toString("base64"), - signers: shouldSign ? undefined : [], // TODO: put auth addr in signers array - authAddr, - })); + const flatTxns = txnsToSign.reduce((acc, val) => acc.concat(val), []); + + const walletTxns: IWalletTransaction[] = flatTxns.map( + ({ txn, shouldSign, authAddr, message }) => ({ + txn: Buffer.from(algosdk.encodeUnsignedTransaction(txn)).toString("base64"), + signers: shouldSign ? undefined : [], // TODO: put auth addr in signers array + authAddr, + message, + }), + ); // sign transaction const requestParams: SignTxnParams = [walletTxns]; const request = formatJsonRpcRequest("algo_signTxn", requestParams); const result: Array = await connector.sendCustomRequest(request); - const signedPartialTxns: Array = result.map((r, i) => { + const indexToGroup = (index: number) => { + for (let group = 0; group < txnsToSign.length; group++) { + const groupLength = txnsToSign[group].length; + if (index < groupLength) { + return [group, index]; + } + + index -= groupLength; + } + + throw new Error(`Index too large for groups: ${index}`); + }; + + const signedPartialTxns: Array> = txnsToSign.map(() => []); + result.forEach((r, i) => { + const [group, groupIndex] = indexToGroup(i); + const toSign = txnsToSign[group][groupIndex]; + if (r == null) { - if (!txnsToSign[i].shouldSign) { - return null; + if (!toSign.shouldSign) { + signedPartialTxns[group].push(null); + return; } throw new Error(`Transaction at index ${i}: was not signed when it should have been`); } - if (!txnsToSign[i].shouldSign) { + if (!toSign.shouldSign) { throw new Error(`Transaction at index ${i} was signed when it should not have been`); } const rawSignedTxn = Buffer.from(r, "base64"); - return new Uint8Array(rawSignedTxn); + signedPartialTxns[group].push(new Uint8Array(rawSignedTxn)); }); - const signedTxns: Uint8Array[] = signedPartialTxns.map((stxn, i) => { - if (stxn) { - return stxn; - } + const signedTxns: Uint8Array[][] = signedPartialTxns.map( + (signedPartialTxnsInternal, group) => { + return signedPartialTxnsInternal.map((stxn, groupIndex) => { + if (stxn) { + return stxn; + } - return signTxnWithTestAccount(txnsToSign[i].txn); - }); + return signTxnWithTestAccount(txnsToSign[group][groupIndex].txn); + }); + }, + ); - const signedTxnInfo: Array<{ + const signedTxnInfo: Array = signedPartialTxns.map((rawSignedTxn, i) => { - if (rawSignedTxn == null) { - return null; - } + } | null>> = signedPartialTxns.map((signedPartialTxnsInternal, group) => { + return signedPartialTxnsInternal.map((rawSignedTxn, i) => { + if (rawSignedTxn == null) { + return null; + } - const signedTxn = algosdk.decodeSignedTransaction(rawSignedTxn); - const txn = (signedTxn.txn as unknown) as algosdk.Transaction; - const txID = txn.txID(); - const unsignedTxID = txnsToSign[i].txn.txID(); + const signedTxn = algosdk.decodeSignedTransaction(rawSignedTxn); + const txn = (signedTxn.txn as unknown) as algosdk.Transaction; + const txID = txn.txID(); + const unsignedTxID = txnsToSign[group][i].txn.txID(); - if (txID !== unsignedTxID) { - throw new Error( - `Signed transaction at index ${i} differs from unsigned transaction. Got ${txID}, expected ${unsignedTxID}`, - ); - } + if (txID !== unsignedTxID) { + throw new Error( + `Signed transaction at index ${i} differs from unsigned transaction. Got ${txID}, expected ${unsignedTxID}`, + ); + } - if (!signedTxn.sig) { - throw new Error(`Signature not present on transaction at index ${i}`); - } + if (!signedTxn.sig) { + throw new Error(`Signature not present on transaction at index ${i}`); + } - return { - txID, - signingAddress: signedTxn.sgnr ? algosdk.encodeAddress(signedTxn.sgnr) : undefined, - signature: Buffer.from(signedTxn.sig).toString("base64"), - }; + return { + txID, + signingAddress: signedTxn.sgnr ? algosdk.encodeAddress(signedTxn.sgnr) : undefined, + signature: Buffer.from(signedTxn.sig).toString("base64"), + }; + }); }); console.log("Signed txn info:", signedTxnInfo); // format displayed result - const formattedResult = { + const formattedResult: IResult = { method: "algo_signTxn", - result: signedTxnInfo, + body: signedTxnInfo, }; // display result @@ -388,7 +425,7 @@ class App extends React.Component { connector, pendingRequest: false, signedTxns, - result: formattedResult || null, + result: formattedResult, }); } catch (error) { console.error(error); @@ -402,20 +439,39 @@ class App extends React.Component { throw new Error("Transactions to submit are null"); } - this.setState({ pendingSubmission: true }); - - try { - const confirmedRound = await apiSubmitTransactions(chain, signedTxns); - if (this.state.pendingSubmission === true) { - this.setState({ pendingSubmission: confirmedRound, pendingSubmissionError: null }); + this.setState({ pendingSubmissions: signedTxns.map(() => 0) }); + + signedTxns.forEach(async (signedTxn, index) => { + try { + const confirmedRound = await apiSubmitTransactions(chain, signedTxn); + + this.setState(prevState => { + return { + pendingSubmissions: prevState.pendingSubmissions.map((v, i) => { + if (index === i) { + return confirmedRound; + } + return v; + }), + }; + }); + + console.log(`Transaction confirmed at round ${confirmedRound}`); + } catch (err) { + this.setState(prevState => { + return { + pendingSubmissions: prevState.pendingSubmissions.map((v, i) => { + if (index === i) { + return err; + } + return v; + }), + }; + }); + + console.error(`Error submitting transaction at index ${index}:`, err); } - console.log(`Transaction confirmed at round ${confirmedRound}`); - } catch (err) { - if (this.state.pendingSubmission === true) { - this.setState({ pendingSubmissionError: err }); - } - console.error("Error submitting transaction:", err); - } + }); } public render = () => { @@ -427,8 +483,7 @@ class App extends React.Component { fetching, showModal, pendingRequest, - pendingSubmission, - pendingSubmissionError, + pendingSubmissions, result, } = this.state; return ( @@ -490,30 +545,48 @@ class App extends React.Component { {"Call Request Approved"} - {Object.keys(result).map(key => ( - - {key} - {JSON.stringify(result[key])} + + Method + {result.method} + + {result.body.map((signedTxns, index) => ( + + {`Atomic group ${index}`} + + {signedTxns.map(txn => ( + <> + {!!txn?.txID &&

TxID: {txn.txID}

} + {!!txn?.signature &&

Sig: {txn.signature}

} + {!!txn?.signingAddress &&

AuthAddr: {txn.signingAddress}

} + + ))} +
))}
this.submitSignedTransaction()} - disabled={pendingSubmission !== false} + disabled={pendingSubmissions.length !== 0} > {"Submit transaction to network."} - {pendingSubmission === true && !pendingSubmissionError && ( - {"Submitting..."} - )} - {typeof pendingSubmission === "number" && ( - {`Transaction confirmed at round ${pendingSubmission}`} - )} - {!!pendingSubmissionError && ( - - {"Transaction rejected by network. See console for more information."} - - )} + {pendingSubmissions.map((submissionInfo, index) => { + const key = `${index}:${ + typeof submissionInfo === "number" ? submissionInfo : "err" + }`; + const prefix = `Txn Group ${index}: `; + let content: string; + + if (submissionInfo === 0) { + content = "Submitting..."; + } else if (typeof submissionInfo === "number") { + content = `Confirmed at round ${submissionInfo}`; + } else { + content = "Rejected by network. See console for more information."; + } + + return {prefix + content}; + })}
) : ( diff --git a/src/scenarios.ts b/src/scenarios.ts index 451f519..8da71f8 100644 --- a/src/scenarios.ts +++ b/src/scenarios.ts @@ -25,15 +25,21 @@ export function signTxnWithTestAccount(txn: algosdk.Transaction): Uint8Array { throw new Error(`Cannot sign transaction from unknown test account: ${sender}`); } -export type Scenario = ( - chain: ChainType, - address: string, -) => Promise>; +export interface IScenarioTxn { + txn: algosdk.Transaction; + shouldSign: boolean; + authAddr?: string; + message?: string; +} + +export type ScenarioReturnType = IScenarioTxn[][]; + +export type Scenario = (chain: ChainType, address: string) => Promise; const singlePayTxn: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const txn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ @@ -45,13 +51,13 @@ const singlePayTxn: Scenario = async ( }); const txnsToSign = [{ txn, shouldSign: true }]; - return txnsToSign; + return [txnsToSign]; }; const singlePayTxnWithClose: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const txn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ @@ -64,13 +70,13 @@ const singlePayTxnWithClose: Scenario = async ( }); const txnsToSign = [{ txn, shouldSign: true }]; - return txnsToSign; + return [txnsToSign]; }; const singlePayTxnWithRekey: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const txn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ @@ -83,13 +89,13 @@ const singlePayTxnWithRekey: Scenario = async ( }); const txnsToSign = [{ txn, shouldSign: true }]; - return txnsToSign; + return [txnsToSign]; }; const singlePayTxnWithRekeyAndClose: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const txn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ @@ -103,13 +109,13 @@ const singlePayTxnWithRekeyAndClose: Scenario = async ( }); const txnsToSign = [{ txn, shouldSign: true }]; - return txnsToSign; + return [txnsToSign]; }; const singleAssetOptInTxn: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const assetIndex = 100; @@ -124,13 +130,13 @@ const singleAssetOptInTxn: Scenario = async ( }); const txnsToSign = [{ txn, shouldSign: true }]; - return txnsToSign; + return [txnsToSign]; }; const singleAssetTransferTxn: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const assetIndex = 11711; // HipoCoin on TestNet @@ -145,13 +151,13 @@ const singleAssetTransferTxn: Scenario = async ( }); const txnsToSign = [{ txn, shouldSign: true }]; - return txnsToSign; + return [txnsToSign]; }; const singleAssetTransferTxnWithClose: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const assetIndex = 100; @@ -167,13 +173,13 @@ const singleAssetTransferTxnWithClose: Scenario = async ( }); const txnsToSign = [{ txn, shouldSign: true }]; - return txnsToSign; + return [txnsToSign]; }; const singleAppOptIn: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const appIndex = 200; @@ -187,13 +193,13 @@ const singleAppOptIn: Scenario = async ( }); const txnsToSign = [{ txn, shouldSign: true }]; - return txnsToSign; + return [txnsToSign]; }; const singleAppCall: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const appIndex = 200; @@ -207,13 +213,13 @@ const singleAppCall: Scenario = async ( }); const txnsToSign = [{ txn, shouldSign: true }]; - return txnsToSign; + return [txnsToSign]; }; const singleAppCallWithRekey: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const appIndex = 200; @@ -228,13 +234,13 @@ const singleAppCallWithRekey: Scenario = async ( }); const txnsToSign = [{ txn, shouldSign: true }]; - return txnsToSign; + return [txnsToSign]; }; const singleAppCloseOut: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const appIndex = 200; @@ -248,13 +254,13 @@ const singleAppCloseOut: Scenario = async ( }); const txnsToSign = [{ txn, shouldSign: true }]; - return txnsToSign; + return [txnsToSign]; }; const sign1FromGroupTxn: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const assetIndex = 100; @@ -284,13 +290,13 @@ const sign1FromGroupTxn: Scenario = async ( algosdk.assignGroupID(txnsToSign.map(toSign => toSign.txn)); - return txnsToSign; + return [txnsToSign]; }; const sign2FromGroupTxn: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const assetIndex = 100; @@ -329,13 +335,13 @@ const sign2FromGroupTxn: Scenario = async ( algosdk.assignGroupID(txnsToSign.map(toSign => toSign.txn)); - return txnsToSign; + return [txnsToSign]; }; const signGroupWithPayOptinTransfer: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const assetIndex = 100; @@ -374,13 +380,13 @@ const signGroupWithPayOptinTransfer: Scenario = async ( algosdk.assignGroupID(txnsToSign.map(toSign => toSign.txn)); - return txnsToSign; + return [txnsToSign]; }; const signGroupWithPayRekey: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const txn1 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ @@ -407,13 +413,13 @@ const signGroupWithPayRekey: Scenario = async ( algosdk.assignGroupID(txnsToSign.map(toSign => toSign.txn)); - return txnsToSign; + return [txnsToSign]; }; const signGroupOf7: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const assetIndex = 100; @@ -494,13 +500,13 @@ const signGroupOf7: Scenario = async ( algosdk.assignGroupID(txnsToSign.map(toSign => toSign.txn)); - return txnsToSign; + return [txnsToSign]; }; const signTxnWithAssetClose: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const assetIndex = 100; @@ -531,13 +537,13 @@ const signTxnWithAssetClose: Scenario = async ( algosdk.assignGroupID(txnsToSign.map(toSign => toSign.txn)); - return txnsToSign; + return [txnsToSign]; }; const signTxnWithRekey: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const assetIndex = 100; @@ -568,13 +574,13 @@ const signTxnWithRekey: Scenario = async ( algosdk.assignGroupID(txnsToSign.map(toSign => toSign.txn)); - return txnsToSign; + return [txnsToSign]; }; const signTxnWithRekeyAndAssetClose: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const assetIndex = 100; @@ -628,13 +634,13 @@ const signTxnWithRekeyAndAssetClose: Scenario = async ( algosdk.assignGroupID(txnsToSign.map(toSign => toSign.txn)); - return txnsToSign; + return [txnsToSign]; }; const fullTxnGroup: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const txnsToSign: Array<{ txn: algosdk.Transaction; shouldSign: boolean }> = []; @@ -667,13 +673,13 @@ const fullTxnGroup: Scenario = async ( algosdk.assignGroupID(txnsToSign.map(toSign => toSign.txn)); - return txnsToSign; + return [txnsToSign]; }; const singlePayTxnWithInvalidAuthAddress: Scenario = async ( chain: ChainType, address: string, -): Promise> => { +): Promise => { const suggestedParams = await apiGetTxnParams(chain); const txn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ @@ -684,8 +690,153 @@ const singlePayTxnWithInvalidAuthAddress: Scenario = async ( suggestedParams, }); - const txnsToSign = [{ txn, shouldSign: true, authAddr:"INVALID_ADDRESS" }]; - return txnsToSign; + const txnsToSign = [{ txn, shouldSign: true, authAddr: "INVALID_ADDRESS" }]; + return [txnsToSign]; +}; + +const multipleNonAtomicTxns: Scenario = async ( + chain: ChainType, + address: string, +): Promise => { + const suggestedParams = await apiGetTxnParams(chain); + + const txn1 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + from: address, + to: testAccounts[0].addr, + amount: 100001, + note: new Uint8Array(Buffer.from("txn 1")), + suggestedParams, + }); + + const txn2 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + from: address, + to: testAccounts[0].addr, + amount: 100002, + note: new Uint8Array(Buffer.from("txn 2")), + suggestedParams, + }); + + const txn3 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + from: address, + to: testAccounts[0].addr, + amount: 100003, + note: new Uint8Array(Buffer.from("txn 3")), + suggestedParams, + }); + + const group1 = [{ txn: txn1, shouldSign: true }]; + // not necessary to do this, but let's test it works + algosdk.assignGroupID(group1.map(toSign => toSign.txn)); + + const group2 = [{ txn: txn2, shouldSign: true }]; + + const group3 = [{ txn: txn3, shouldSign: true }]; + + return [group1, group2, group3]; +}; + +const atomicGroupAndNonAtomicTxns: Scenario = async ( + chain: ChainType, + address: string, +): Promise => { + const suggestedParams = await apiGetTxnParams(chain); + + const txn1 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + from: address, + to: testAccounts[0].addr, + amount: 100001, + note: new Uint8Array(Buffer.from("atomic group 1 txn 1")), + suggestedParams, + }); + + const txn2 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + from: address, + to: testAccounts[0].addr, + amount: 100002, + note: new Uint8Array(Buffer.from("atomic group 2 txn 2")), + suggestedParams, + }); + + const txn3 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + from: address, + to: testAccounts[0].addr, + amount: 100003, + note: new Uint8Array(Buffer.from("txn 3")), + suggestedParams, + }); + + const txn4 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + from: address, + to: testAccounts[0].addr, + amount: 100004, + note: new Uint8Array(Buffer.from("txn 4")), + suggestedParams, + }); + + const group1 = [ + { txn: txn1, shouldSign: true }, + { txn: txn2, shouldSign: true }, + ]; + algosdk.assignGroupID(group1.map(toSign => toSign.txn)); + + const group2 = [{ txn: txn3, shouldSign: true }]; + + const group3 = [{ txn: txn4, shouldSign: true }]; + + return [group1, group2, group3]; +}; + +const multipleAtomicGroups: Scenario = async ( + chain: ChainType, + address: string, +): Promise => { + const suggestedParams = await apiGetTxnParams(chain); + + const txn1 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + from: address, + to: testAccounts[0].addr, + amount: 100001, + note: new Uint8Array(Buffer.from("atomic group 1 txn 1")), + suggestedParams, + }); + + const txn2 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + from: address, + to: testAccounts[0].addr, + amount: 100002, + note: new Uint8Array(Buffer.from("atomic group 1 txn 2")), + suggestedParams, + }); + + const txn3 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + from: address, + to: testAccounts[0].addr, + amount: 100003, + note: new Uint8Array(Buffer.from("atomic group 2 txn 1")), + suggestedParams, + }); + + const txn4 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + from: address, + to: testAccounts[0].addr, + amount: 100004, + note: new Uint8Array(Buffer.from("atomic group 2 txn 2")), + suggestedParams, + }); + + const group1 = [ + { txn: txn1, shouldSign: true }, + { txn: txn2, shouldSign: true }, + ]; + algosdk.assignGroupID(group1.map(toSign => toSign.txn)); + + const group2 = [ + { txn: txn3, shouldSign: true }, + { txn: txn4, shouldSign: true }, + ]; + algosdk.assignGroupID(group2.map(toSign => toSign.txn)); + + return [group1, group2]; }; export const scenarios: Array<{ name: string; scenario: Scenario }> = [ @@ -771,6 +922,18 @@ export const scenarios: Array<{ name: string; scenario: Scenario }> = [ }, { name: "21. Single pay txn with invalid auth address", - scenario: singlePayTxnWithInvalidAuthAddress - } + scenario: singlePayTxnWithInvalidAuthAddress, + }, + { + name: "22. Sign multiple non-atomic txns", + scenario: multipleNonAtomicTxns, + }, + { + name: "23. Sign atomic txn group and non-atomic txns", + scenario: atomicGroupAndNonAtomicTxns, + }, + { + name: "24. Sign multiple atomic txn groups", + scenario: multipleAtomicGroups, + }, ];