Templated Wallets
DRAFT
This is a draft document describing the tentative approach that CashConnect will use for Templated Wallets. Some parts may be outdated: This was written late July, 2025.
Overview
The ideal here is that any Template that can be used in a wallet can also be used for a Dapp.
So, for example, there might be a site at https://multisig.cash that can basically act as a Multisig Wallet using a Multisig Template - albeit in browser. One thing to keep in mind with this is that Dapps will often require templates to contain utility functions (actions): E.g. getPublicKey or, for Dapps like Bull, signForGetContractStatus. So we don't want to instantiate templates as such - instead, we execute actions which return state (which, in some cases, subsequent actions may use).
When we use "Templates as Wallets" inside a Wallet, these are basically implemented in the same way you would a Dapp on the web. The difference is that our Wallet Implementation generalizes such that it can work with Templates that satisfy a given interface. To give a more concrete example of this, imagine the following flow:
- I import a Template into a Wallet.
- I have an action (entry point as it has no dependencies) called createWallet
- I click on this action and, when executed, this returns a payload something like:
// I.e. The "createWallet" actions returns the "Wallet State".
{
// Type of this state is a "wallet".
// NOTE: Not sure we would have this in practice, but it simplifies this example.
type: "wallet",
// ... other data relevant to this wallet.
// ... e.g. Maybe it's a Passworded Wallet.
}The Wallet, being a "clever wallet", sees that the payload returned from this createWallet execution is of "type" wallet. It says, "oh, I should be able to handle this more natively!" and...
It then does something something like so:
// Where deps represents injection dependencies: Blockchain Connection, Store, etc.
const newWallet = new WalletTemplated(deps, theTemplateUsed, actionResponse); // actionResponse = the "Wallet State"
// TODO: Save and add to our list of WalletsThe WalletTemplated class above might look something like this (this is the important part):
// NOTE: Still in heavy flux, but for current Base interface see here:
// https://gitlab.com/GeneralProtocols/xo/stack/-/blob/development/packages/stack/src/wallet/base-wallet.ts?ref_type=heads
class WalletTemplated extends BaseWallet {
constructor(public deps, public template, public walletData) {
// ...
}
async getUnspents() {
// NOTE: For many wallets, we probably do not require much more than this to track their UTXOs.
// We just need to know their addresses (in practice, lockingBytecode, but simplifying here).
const addresses = this.template.executeAction('getAddresses', this.walletData);
// Return the unspents for this wallet.
return await this.blockchain.fetchUnspents(addresses);
}
async getBalanceSats() {
// Get unspents.
const unspents = await this.getUnspents();
// Return the satoshi balance.
return calculateBalanceSats(unspents);
}
async sendTransaction(txData) {
// NOTE: Not sure what this action interface would look like yet.
// Probably more versatile to have a "signOutputs" or similar.
const tx = await this.template.executeAction('sendTransaction', {
...this.walletData
...txData,
});
// Broadcast the tx
await this.blockchain.broadcastTransaction(tx);
}
// Might eventually want something like this to execute other actions in the template that "use" the state that createWallet returns.
async getOtherActions() {
// ...
}
// ... etc
}Note in the above that we wrap the template such that it conforms to our BaseWallet interface. This is what allows it to work seamlessly with the rest of the Wallet.
The advantage of the above is that it's really quite simple. Our Templates that behave "as-a-wallet" can then be reduced to something more like this:
const template = {
name: "P2PKH Example",
actions: {
// Create Action
createWallet: {
name: "Create P2PKH Wallet",
variables: {
// NOTE: CashASM requires that we specify the HD index to be used as data.
index: {
name: "HD Index",
type: "number"
},
},
returns: {
// TODO: In practice, we may need to annotate with a var that this can be treated as a Wallet (for intra-wallet handling).
// Or we might be able to just infer that from action interfaces of the template. TBD.
index: {
name: "HD Index",
type: "number"
}
},
},
// Send Action (in practice, this might be a different instruction type than "transaction". Maybe something like "signOutputs")
sendTransaction: {
// Uses the state returned by `createWallet` (could maybe call this "dependsOn")
uses: ['createWallet'],
variables: {
// ...
},
instructions: {
// ...
},
returns: {
// ...
}
}
// Get Addresses for this wallet (technically, we wouldn't actually return addresses, but probably lockingBytecode).
getAddresses: {
// Uses the state returned by `createWallet` (could maybe call this "dependsOn")
uses: ['createWallet'],
variables: {
// ...
},
instructions: {
// ...
},
returns: {
// ...
}
}
},
scripts: {
lock: {
lockingType: "standard",
name: "P2PKH Lock",
script:
"OP_DUP OP_HASH160 <$(<sandbox.public_key> OP_HASH160)> OP_EQUALVERIFY OP_CHECKSIG",
},
unlock: {
name: "Unlock",
script: "<sandbox.schnorr_signature.all_outputs> <sandbox.public_key>",
unlocks: "lock",
},
}
}So, we get the state tracking this way in the same way a Dapp would be able to do it: Our Intra-Wallet handling is essentially just a Dapp that generalizes to a particular interface provided by templates: albeit it is seamlessly integrated within the Wallet itself (ideally, all our Wallets should use Templates). The trick here really is defining good action and BaseWallet interfaces for it such that it can accommodate a wide-range of those intra-wallet use-cases (I probably won't dive into that until after I get the Dapp-cases sorted though).
Appendix
Extending TemplatedWallet to specialize Templates
Just to give a very rough gist on how we then specialize for specific templates (DO NOT TAKE CODE LITERALLY):
// NOTE: We extend our WalletTemplated class.
class WalletMultisig extends WalletTemplated {
// We'd probably want to specialize instantiation because that would have to communicate with peers (for good UX).
static async create(...) {
// TODO: Show dialog with links to send to Peer A and Peer B so that they can connect to us.
// TODO: Wait for them to connect.
// TODO: Request their public keys.
const peerAPubKey = await peerAConnection.sendActionRequest('getPublicKey', data);
const peerBPubKey = await peerBConnection.sendActionRequest('getPublicKey', data);
// Execute the createWallet action
template.executeAction('createWallet',
}
async sendTransaction(txData) {
// We need to propose a Tx for other parties to sign.
await proposedTx = await this.template.executeAction('proposeTransaction', txData);
// Send proposed tx to peers B and C so that they can sign it.
const peerASig = await this.peerAConnection.sendActionRequest('signTransaction', proposeTxData);
const peerBSig = await this.peerBConnection.sendActionRequest('signTransaction', proposeTxData);
// We now have the require data to build the transaction.
const tx = this.template.executeAction('sendTransaction', {
...this.walletState,
...proposedTx,
...peerASig,
...peerBSig,
})
// Broadcast the Tx.
await this.blockchain.broadcastTransaction(tx);
}
//.NOTE: Most other methods "probably" just extend from WalletTemplated so we don't need to override them.
// We only really need to override the parts where we want UX conveniences (e.g. P2P communication so that the user doesn't have to manually input info from peers).
}