Create transactions for Kadena SpireKey
If you enable your application to connect to Kadena SpireKey as described in Integrate decentralized apps, you can construct transactions for users to sign using their Kadena SpireKey account.
In this guide you'll learn:
- How to construct an unsigned transaction to support signing using a Kadena SpireKey account.
- How to send unsigned transactions to the SpireKey URL for signing.
- How to process the data you receive in signed transaction returned from Kadena SpireKey.
Construct an unsigned transaction
Transactions that can be signed using a Kadena SpireKey account are similar to transactions that are signed using other wallets. At a high level, you construct the transaction with the appropriate information, format the information as a JSON request, and submit the JSON request to a Chainweb node endpoint.
However, transactions involving Kadena SpireKey accounts have some unique requirements.
Public key generation
When users register an account using Kadena SpireKey, the cryptographic algorithm
used to generate the public and secret keys is different from the cryptographic
algorithm and ED25519 signature scheme used to generate the public and secret keys for other Kadena accounts.
To differentiate Kadena SpireKey account public keys from other public
keys, transactions must include both the public key and the scheme attribute
set to WebAuthn as its value when signing transactions.
{ pubKey: webAuthnPublicKey, scheme: 'WebAuthn' }{ pubKey: webAuthnPublicKey, scheme: 'WebAuthn' }Account guard
When users create an account for the coin contract or for other fungible-v2 contracts, they provide an account name and a guard.
As mentioned in Account, keys, and principals, there are different types of guards, but keysets are the most commonly-used for accounts that hold assets.
Kadena SpireKey accounts are different in that they use a capability defined in the webauthn-wallet contract instead of a keyset to enforce the rules for signing transactions.
Because the coin contract can't bring this capability into scope when trying to debit an account, the webauthn-wallet contract implements its own webauthn-wallet.transfer function and custom webauthn-wallet.GAS, webauthn-wallet.GAS_PAYER, and webauthn-wallet.TRANSFER capabilities.
The webauthn-wallet contract wraps the coin contract to bring the required capabilities into scope before calling the wrapped functions.
You can use the webauthn-wallet.GAS, webauthn-wallet.GAS_PAYER, and webauthn-wallet.TRANSFER capabilities in place of the corresponding coin.GAS and coin.TRANSFER capabilities to satisfy the guard required to debit an account.
Because Chainweb node requires signatures for the capabilities brought into scope, Kadena SpireKey accounts need to sign for both the webauthn-wallet.GAS capability and the webauthn-wallet.GAS_PAYER capability to pay for gas.
The following is a simplified example of what an unsigned transfer transaction from
a Kadena SpireKey account might look like with the cmd field in non-stringified JSON format for readability:
{ "cmd": { "payload": { "exec": { "code": "(n_eef68e581f767dd66c4d4c39ed922be944ede505.webauthn-wallet.transfer \"c:bF51UeSqhrSjEET1yUWBYabDTfujlAZke4R70I4rrHc\" \"k:9cb650e653f563d782182a67b73a4d5d553aaf6f1c4928087bb7d91d59b8a227\" 2.00000000000)", "data": {} } }, "nonce": "kjs:nonce:1710872658811", "signers": [ { "pubKey": "WEBAUTHN-a50102032620012158200df3845d4ad0f626a3c860715ad3d4bd7bbee03330aa32878d6baa045e98f64f2258206a93722f35f3d0692dc4c26703653498eae51816ffb7b70e4670b010103bd9eb", "scheme": "WebAuthn", "clist": [ { "name": "n_eef68e581f767dd66c4d4c39ed922be944ede505.webauthn-wallet.GAS_PAYER", "args": [ "c:bF51UeSqhrSjEET1yUWBYabDTfujlAZke4R70I4rrHc", { "int": 1 }, 1 ] }, { "name": "n_eef68e581f767dd66c4d4c39ed922be944ede505.webauthn-wallet.TRANSFER", "args": [ "c:bF51UeSqhrSjEET1yUWBYabDTfujlAZke4R70I4rrHc", "k:9cb650e653f563d782182a67b73a4d5d553aaf6f1c4928087bb7d91d59b8a227", { "decimal": "2.00000000000" } ] } ] } ], "meta": { "gasLimit": 2000, "gasPrice": 1e-7, "sender": "c:bF51UeSqhrSjEET1yUWBYabDTfujlAZke4R70I4rrHc", "ttl": 60000, "creationTime": 1710872658, "chainId": "1" }, "networkId": "testnet04" }, "hash": "6DGS9ML91o6S9BLgo_lBzLPkRZSb5RImCL06zjrbkD0", "sigs": [null]}{ "cmd": { "payload": { "exec": { "code": "(n_eef68e581f767dd66c4d4c39ed922be944ede505.webauthn-wallet.transfer \"c:bF51UeSqhrSjEET1yUWBYabDTfujlAZke4R70I4rrHc\" \"k:9cb650e653f563d782182a67b73a4d5d553aaf6f1c4928087bb7d91d59b8a227\" 2.00000000000)", "data": {} } }, "nonce": "kjs:nonce:1710872658811", "signers": [ { "pubKey": "WEBAUTHN-a50102032620012158200df3845d4ad0f626a3c860715ad3d4bd7bbee03330aa32878d6baa045e98f64f2258206a93722f35f3d0692dc4c26703653498eae51816ffb7b70e4670b010103bd9eb", "scheme": "WebAuthn", "clist": [ { "name": "n_eef68e581f767dd66c4d4c39ed922be944ede505.webauthn-wallet.GAS_PAYER", "args": [ "c:bF51UeSqhrSjEET1yUWBYabDTfujlAZke4R70I4rrHc", { "int": 1 }, 1 ] }, { "name": "n_eef68e581f767dd66c4d4c39ed922be944ede505.webauthn-wallet.TRANSFER", "args": [ "c:bF51UeSqhrSjEET1yUWBYabDTfujlAZke4R70I4rrHc", "k:9cb650e653f563d782182a67b73a4d5d553aaf6f1c4928087bb7d91d59b8a227", { "decimal": "2.00000000000" } ] } ] } ], "meta": { "gasLimit": 2000, "gasPrice": 1e-7, "sender": "c:bF51UeSqhrSjEET1yUWBYabDTfujlAZke4R70I4rrHc", "ttl": 60000, "creationTime": 1710872658, "chainId": "1" }, "networkId": "testnet04" }, "hash": "6DGS9ML91o6S9BLgo_lBzLPkRZSb5RImCL06zjrbkD0", "sigs": [null]}In this example, the n_eef68e581f767dd66c4d4c39ed922be944ede505 namespace is the namespace for Kadena SpireKey where the webauthn-wallet and webauthn-guard contracts are deployed on the Kadena test and main networks.
The transaction sender is a Kadena SpireKey account with the account name c:bF51UeSqhrSjEET1yUWBYabDTfujlAZke4R70I4rrHc.
To construct a transaction similar to this example in your application, you use the c: account and WEBAUTHN public key returned by Kadena SpireKey in the accountName and credentials properties from the account details for a user object.
Send transactions to Kadena SpireKey
After you construct the unsigned transaction in your application, you need to base64 encode the stringified JSON of the transaction and send a signing request to Kadena SpireKey.
To construct the route:
-
Stringify the unsigned transaction (tx) you've prepared:
const encodedTx = btoa(JSON.stringify(tx));const encodedTx = btoa(JSON.stringify(tx)); -
Use the
encodeURIComponentfunction to encode the return URL:const encodedReturnUrl = encodeURIComponent(RETURN_URL);const encodedReturnUrl = encodeURIComponent(RETURN_URL); -
Construct the request URL using the
encodedTxandencodedReturnUrlto send the transaction to Kadena SpireKey:const sendTransactionUrl = `https://spirekey.kadena.io/sign#transaction=${encodedTx}&returnUrl=${encodedReturnUrl}`;const sendTransactionUrl = `https://spirekey.kadena.io/sign#transaction=${encodedTx}&returnUrl=${encodedReturnUrl}`;
Because transaction strings can be longer than what is accepted in searchParameters, you should send transactions to the /sign endpoint using the anchor hashtag (#) instead of the searchParameters question mark (?).
For example, a signature request might look similar to this:
The following table describes the parameters that you can include in your signing request:
| Parameter | Type | Required | Description |
|---|---|---|---|
transaction | string | Required | A base64 encoded string of the unsigned transaction. |
returnUrl | string | Required | The url, encoded as a uriComponent, that the Kadena SpireKey should redirect users to after they have signed the transaction. |
translations | string | Optional | Custom descriptions that explain what capabilities or operations that the user is signing for. |
optimistic | boolean | Optional | Allows applications to continue transaction workflows without having to wait for the transaction to be confirmed on the blockchain. When this parameter is included, pendingTxIds are returned so that the application can keep track of the status of the submitted transactions and update the UI accordingly. For more information about the optimistic transaction flow, see Optimistic account onboarding. |
After you construct the request with the required parameters, you can navigate to Kadena SpireKey to enable the user to sign the transaction.
Verify the signed transaction
After signing the transaction, the user is redirected to the returnUrl you provided.
The signed transaction and optional parameters are returned as parameters using the anchor hashtag (#).
If the transaction was not successfully signed, the unsigned transaction is returned.
| Parameter | Type | Required | Description |
|---|---|---|---|
transaction | string | Required | A base64 encoded string of the signed or unsigned transaction. |
pendingTxIds | string[] | Optional | Pending transaction identifiers that enable the application to move forward without waiting for the transaction to be confirmed on the blockchain. |
To verify that a transaction has been successfully signed, you can check the sigs field in the transaction.
If the field has undefined signatures, you won't be able to submit the transaction.
If the transaction has been successfully signed, it is valid for the period of time set in the ttl value in the transaction publicMeta data.
During this period of time, the application can call the local?preflight=true endpoint to check whether the transaction is valid, then send it to the blockchain to be executed.
After the transaction is submitted to the blockchain, you can check the transaction status using the request key and the Kadena client. For example, you can poll for the transaction status using Kadena client:
const results = pollStatus(transactionDescriptors, { onPoll: (requestKey) => { console.log('polling status of', requestKey); },});const results = pollStatus(transactionDescriptors, { onPoll: (requestKey) => { console.log('polling status of', requestKey); },});