#Signed Transaction User Authorization
23 messages · Page 1 of 1 (latest)
Hi @keen bane. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
To achieve user authorization via a signed transaction using AlgoKit or algosdk in TypeScript, you can follow these general steps:
1. Create an Unsigned Transaction
Use AlgoKit Utils or algosdk to create an unsigned transaction. For example, with AlgoKit Utils:
const paymentTxn = await algorand.createTransaction.payment({
sender: userAddress,
receiver: backendAddress,
amount: 0, // Use a 0-Algo payment for authorization (no funds transferred)
note: 'Authorization request',
});
Creating and issuing transactions
2. Let the User Sign the Transaction
If you are building a dApp, you typically use a wallet integration (e.g., WalletConnect, MyAlgo, Pera) to prompt the user to sign the transaction. The transaction is serialized (msgpack-encoded and base64) and sent to the wallet for signing, following the ARC-0001 or ARC-0025 standards.
Example WalletTransaction object for signing:
const walletTransaction = {
txn: Buffer.from(algosdk.encodeObj({ txn: paymentTxn.get_obj_for_encoding() })).toString('base64'),
// Optionally, add signers, message, etc.
};
3. Send the Signed Transaction to the Backend
After the user signs the transaction, you receive a base64-encoded signed transaction (SignedTxnStr). Send this string to your backend.
4. Backend: Decode and Validate the Signed Transaction
On the backend, decode the signed transaction and verify the signature matches the expected user address. In Python, you would use py-algorand-sdk, but in TypeScript you can use algosdk:
const signedTxnBytes = Buffer.from(signedTxnBase64, 'base64');
const decoded = algosdk.decodeSignedTransaction(signedTxnBytes);
const sender = decoded.txn.from; // The address that signed the transaction
You can then check that sender matches the expected user address, confirming ownership.
Notes
- AlgoKit CLI can also be used for signing transactions via the command line, but for web/dApp flows, wallet integrations are standard.
- The transaction does not need to be submitted to the blockchain for this kind of authorization; just signing is sufficient for proof of ownership.
- For more details on transaction signing and encoding, see AlgoKit Task Sign and Encoding transactions for signing.
If you need a full code example for a specific wallet integration, let me know!
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@keen bane Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@rain cave Is there anything else that can be added to strengthen the validation. Right now it's just testing sender but are there other ways that I can validate that the wallet signed the transaction?
Let me check the knowledge base for you again. One moment! :books:
To strengthen validation beyond just checking the sender, you should consider the following best practices, as outlined in the ARC-0001 standard:
-
Verify the Signature: Ensure that the signature on the transaction is valid for the sender address. This is implicit when decoding a signed transaction using the SDK, but you should explicitly check that the signature matches the expected public key.
-
Check Transaction Fields: Validate every field in the transaction. The ARC-0001 standard requires that all fields must be known and valid; any extra or unknown fields must cause the transaction to be rejected. This prevents maliciously crafted transactions from introducing unexpected behavior or vulnerabilities. For example, ensure there are no unexpected fields like
RekeyToorCloseRemainderTounless you specifically intend to allow them, and display strong warnings if they are present and being signed Transaction Validation. -
Genesis Hash and Network Validation: Check that the transaction’s
GenesisHashand (if present)GenesisIDmatch the network you expect. This ensures the transaction is intended for the correct Algorand network and not a different one Genesis Validation. -
Replay Protection: Use fields like
firstValid,lastValid, andleaseto ensure the transaction cannot be replayed. For example, a unique lease value can prevent the same transaction from being submitted multiple times Code Example.
-
Manual Validation: Do not rely solely on SDK or TypeScript typing for validation. Manually verify all fields after deserialization, as SDKs are not designed to handle maliciously crafted transactions General Validation.
-
Check for Multisig or Rekeying: If the transaction uses multisig or rekeying, ensure you validate the
msigandauthAddrfields according to the rules in ARC-0001. For example, ifauthAddris present, the wallet must sign using that address, and you should verify that the signature matches Semantic of WalletTransaction.
By following these steps, you can significantly strengthen your validation and reduce the risk of unauthorized or malicious transactions being accepted.
If you need more specific code examples or have a particular scenario in mind, let me know!
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@keen bane Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@rain cave
export const signedZeroTxn = async (
algorand: AlgorandClient,
signTransactions: TransactionSigner,
activeAddress: string
): Promise<string | null> => {
if (!activeAddress) return null
try {
// Create transaction
const suggestedParams = await algorand.client.algod
.getTransactionParams()
.do()
const transaction = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
sender: activeAddress,
receiver: activeAddress,
amount: 0,
note: Buffer.from(`auth-${Date.now()}-${Math.floor(Math.random() * 1000000)}`),
suggestedParams,
})
let signedTxns: (Uint8Array | null)[]
if (import.meta.env.VITE_ALGO_NETWORK === 'localnet') {
// Use environment variable mnemonic for localnet
const mnemonic = import.meta.env.VITE_ALPHAWORD_MNEMONIC || ''
if (!mnemonic) {
throw new Error('No mnemonic found for localnet signing')
}
const account = algosdk.mnemonicToSecretKey(mnemonic)
const signedTxn = transaction.signTxn(account.sk)
signedTxns = [signedTxn]
} else {
signedTxns = await signTransactions([transaction])
}
// Return base64 encoded signed transaction
return Buffer.from(signedTxns[0]!).toString('base64')
} catch (error) {
console.error('Error signing transaction:', error)
return null
}
}
based on this code, where the Algo Network is localnet, how can I verify the signed transaction?
Here I've looked at https://developer.algorand.org/docs/get-details/transactions/offline_transactions/?from_query=verify signed transaction#signature-verification which contains signature verification. Will this work with algosdk version 3 and up? And if it does can you right the verification function?
Let me check the knowledge base for you again. One moment! :books:
Based on your code and the knowledge sources provided, here’s how you can verify a signed transaction using algosdk (version 3 and up):
1. Decoding the Signed Transaction
You can use algosdk.decodeSignedTransaction to decode the base64-encoded signed transaction. This will give you access to the transaction object and the signature.
2. Verifying the Signature
You can use algosdk.verifyBytes to verify the signature. This function takes the bytes of the transaction, the signature, and the public key, and returns a boolean indicating if the signature is valid.
Example Verification Function
import algosdk from 'algosdk';
export function verifySignedTxnBase64(signedTxnBase64: string, expectedAddress: string): boolean {
// Decode the signed transaction
const signedTxnBytes = Buffer.from(signedTxnBase64, 'base64');
const decoded = algosdk.decodeSignedTransaction(signedTxnBytes);
// Get the transaction bytes (to be verified)
const txnBytes = decoded.txn.toByte();
// Get the signature
const signature = decoded.sig;
// Get the public key from the expected address
const pk = algosdk.decodeAddress(expectedAddress).publicKey;
// Verify the signature
return algosdk.verifyBytes(txnBytes, signature, pk);
}
This approach is supported by the functions available in algosdk v3 and up, as listed in the js-algorand-sdk documentation.
Note:
- The function above assumes a single signature (not multisig or logic sig).
- The verification process matches the signature to the transaction and the public key derived from the address, which is the standard way to verify ownership of the private key that signed the transaction.
If you need to handle multisig or logic sig, additional steps are required, but for your use case (a simple 0-Algo payment signed by a user), this is sufficient and aligns with Algorand’s recommended practices.
References:
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@keen bane Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@rain cave
/**
- This type represents a function which can sign transactions from an atomic transaction group.
- @param txnGroup - The atomic group containing transactions to be signed
- @param indexesToSign - An array of indexes in the atomic transaction group that should be signed
- @returns A promise which resolves an array of encoded signed transactions. The length of the
- array will be the same as the length of indexesToSign, and each index i in the array
- corresponds to the signed transaction from txnGroup[indexesToSign[i]]
*/
export type TransactionSigner = (txnGroup: Transaction[], indexesToSign: number[]) => Promise<Uint8Array[]>;
what does indexesToSign do?
Does this mean that if I have an array of transactions to sign, the indexesToSign specifies which of those transactions to sign? What would it look like if I only had 1 transaction to sign?
Let me check the knowledge base for you again. One moment! :books:
Yes, your understanding is correct. The indexesToSign parameter specifies which transactions in the txnGroup array should be signed.
According to the js-algorand-sdk documentation:
An array of indexes in the atomic transaction group that should be signed
The function will return an array of signed transactions, where each element corresponds to the transaction at the index specified in indexesToSign from the txnGroup array.
If you only have one transaction to sign:
txnGroupwould be an array with a single transaction:[txn]indexesToSignwould be[0](since the only transaction is at index 0)
So, for a single transaction, you would call:
signTransactions([txn], [0])
This will return a promise resolving to an array with one signed transaction, corresponding to the transaction at index 0 in txnGroup TransactionSigner type.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@keen bane Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@rain cave
This is still returning false for verification, what else could be wrong?
Let me check the knowledge base for you again. One moment! :books:
If your signature verification is still returning false, there are several possible reasons based on the knowledge sources:
-
Domain Separation Prefix: Algorand uses domain separation when signing and verifying. For standard transaction signatures, the message should be prepended with
"TX"before signing, and forverifyBytes(as in the SDK), the message should be prepended with"MX"for multisig or"TX"for single sig, depending on the context. If the domain separation prefix is incorrect or missing, verification will fail see AVM Step Function. -
Incorrect Bytes for Verification: The bytes you verify must be exactly the same as those that were signed. For transactions, this means the canonical msgpack encoding of the transaction, with the correct domain separation prefix. If you use
txn.toByte()or similar, ensure it matches the bytes that were actually signed see js-algorand-sdk verifyBytes. -
Wrong Public Key: Make sure you are using the correct public key (derived from the correct address) for verification. If the address is not the actual signer, verification will fail.
-
Signature Extraction: Ensure you are extracting the signature from the correct field. For a standard signed transaction, it should be in the
sigfield of the decoded signed transaction object see js-algorand-sdk decodeSignedTransaction. -
Transaction Modification: If the transaction was modified in any way after signing (even a single byte), the signature will not match.
- Network/Genesis Hash: If the transaction was created for a different network (e.g., testnet vs. mainnet vs. localnet), the genesis hash in the transaction may differ, resulting in a different transaction byte encoding and thus a failed verification see ARC-0001 Genesis Validation.
If you have checked all of the above and verification still fails, you may want to:
- Double-check that you are using the correct domain separation prefix and transaction bytes.
- Ensure the transaction is not being altered between signing and verification.
- Confirm that the address used for verification matches the one that signed the transaction.