2 Commits

12 changed files with 2544 additions and 4748 deletions

904
p2pkh-template.json Normal file
View File

@@ -0,0 +1,904 @@
{
"$schema": "https://libauth.org/schemas/wallet-template-v0.schema.json",
"name": "Wallet (P2PKH)",
"description": "A standard single-factor wallet template that uses Pay-to-Public-Key-Hash (P2PKH) locking scripts.",
"icon": "wallet",
"version": 0,
"supported": ["BCH_2023_05", "BCH_2024_05", "BCH_2025_05", "BCH_2026_05"],
"roles": {
"owner": {
"name": "Wallet Owner",
"description": "The party who can spend from this wallet.",
"icon": "owner"
},
"receiver": {
"name": "Receiver",
"description": "A party that is receiving value.",
"icon": "receiver"
},
"sender": {
"name": "Sender",
"description": "A party that is sending value.",
"icon": "sender"
}
},
"start": [
{ "action": "receive", "role": "receiver" },
{ "action": "requestSatoshis", "role": "receiver" },
{ "action": "requestFungibleTokens", "role": "receiver" },
{ "action": "requestNonfungibleTokens", "role": "receiver" }
],
"actions": {
"receive": {
"name": "Receive",
"description": "Receive an unspecified amount of cash and/or tokens from one or more senders.",
"icon": "receive",
"roles": {
"receiver": {
"name": "Receive",
"description": "Receive an unspecified amount of cash and/or tokens from one or more senders.",
"icon": "receive",
"requirements": { "generate": ["ownerKey"] }
},
"sender": {
"name": "Send",
"description": "Send an unspecified amount of cash and/or tokens to the provided receiver.",
"icon": "send"
}
},
"requirements": {
"roles": [
{ "role": "receiver", "slots": { "min": 1, "max": 1 } },
{ "role": "sender", "slots": { "min": 1 } }
],
"variables": ["requestedSatoshis"]
},
"transaction": "receiveTransaction"
},
"requestSatoshis": {
"name": "Request Satoshis",
"description": "Requests a specific amount of Bitcoin Cash from one or more senders.",
"icon": "request",
"roles": {
"receiver": {
"name": "Request Satoshis",
"description": "Requests a specific amount of Bitcoin Cash from one or more senders.",
"icon": "request",
"requirements": {
"generate": ["ownerKey"],
"variables": ["requestedSatoshis"]
}
},
"sender": {
"name": "Send",
"description": "Send a specific amount of Bitcoin Cash to the provided receiver.",
"icon": "send"
}
},
"requirements": {
"roles": [
{ "role": "receiver", "slots": { "min": 1, "max": 1 } },
{ "role": "sender", "slots": { "min": 1 } }
]
},
"transaction": "requestSatoshisTransaction"
},
"requestFungibleTokens": {
"name": "Request Fungible Tokens",
"description": "Requests a specific amount of a fungible tokens from one or more senders.",
"icon": "request",
"roles": {
"receiver": {
"name": "Request Fungible Tokens",
"description": "Requests a specific amount of a fungible tokens from one or more senders.",
"icon": "request",
"requirements": {
"generate": ["ownerKey"],
"variables": ["requestedTokenCategory", "requestedTokenAmount"]
}
},
"sender": {
"name": "Send",
"description": "Send a specific amount of fungible tokens to the provided receiver.",
"icon": "send"
}
},
"requirements": {
"roles": [
{ "role": "receiver", "slots": { "min": 1, "max": 1 } },
{ "role": "sender", "slots": { "min": 1 } }
]
},
"transaction": "requestFungibleTokensTransaction"
},
"requestNonfungibleTokens": {
"name": "Request a Non-fungible Token",
"description": "Requests a non-fungible token from one or more senders.",
"icon": "request",
"roles": {
"receiver": {
"name": "Request a Non-fungible Token",
"description": "Requests a non-fungible token from one or more senders.",
"icon": "request",
"requirements": {
"generate": ["ownerKey"],
"variables": [
"requestedTokenCategory",
"requestedTokenCapability",
"requestedTokenCommitment"
]
}
},
"sender": {
"name": "Send",
"description": "Send a non-fungible token to the provided receiver.",
"icon": "send"
}
},
"requirements": {
"roles": [
{ "role": "receiver", "slots": { "min": 1, "max": 1 } },
{ "role": "sender", "slots": { "min": 1 } }
]
},
"transaction": "requestNonfungibleTokensTransaction"
},
"sendSatoshis": {
"name": "Send Satoshis",
"description": "Sends a specific amount of Bitcoin Cash to a given recipient.",
"icon": "send",
"roles": {
"sender": {
"requirements": {
"variables": ["transferredSatoshis", "recipientLockingscript"],
"secrets": ["ownerKey"]
}
}
},
"requirements": {
"roles": [{ "role": "sender", "slots": { "min": 1, "max": 1 } }]
},
"condition": "$(OP_INPUTINDEX OP_UTXOVALUE <dustLimit> OP_GREATERTHAN)",
"transaction": "transferSatoshisTransaction"
},
"sendFungibleTokens": {
"name": "Send Fungible Tokens",
"description": "Send a specific amount of a fungible token to a given recipient.",
"icon": "send",
"roles": {
"sender": {
"requirements": {
"variables": [
"transferredTokenCategory",
"transferredTokenAmount",
"recipientLockingscript"
],
"secrets": ["ownerKey"]
}
}
},
"requirements": {
"roles": [{ "role": "sender", "slots": { "min": 1, "max": 1 } }]
},
"condition": "$(OP_INPUTINDEX OP_UTXOTOKENAMOUNT <0> OP_GREATERTHAN)",
"transaction": "transferFungibleTokensTransaction"
},
"sendNonfungibleTokens": {
"name": "Send a Non-fungible Token",
"description": "Send a non-fungible token to a given recipient.",
"icon": "send",
"roles": {
"sender": {
"requirements": {
"variables": [
"transferredTokenCategory",
"transferredTokenCapability",
"transferredTokenCommitment",
"recipientLockingscript"
],
"secrets": ["ownerKey"]
}
}
},
"requirements": {
"roles": [{ "role": "sender", "slots": { "min": 1, "max": 1 } }]
},
"condition": "$(OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_SIZE OP_NIP <32> OP_GREATERTHAN)",
"transaction": "transferNonfungibleTokensTransaction"
},
"burnFungibleTokens": {
"name": "Delete Fungible Tokens",
"description": "Permanently and irreversibly deletes one or more fungible tokens.",
"icon": "burn",
"roles": {
"owner": {
"requirements": {
"variables": ["burnedTokenCategory", "burnedTokenAmount"],
"secrets": ["ownerKey"]
}
}
},
"requirements": {
"roles": [{ "role": "owner", "slots": { "min": 1, "max": 1 } }]
},
"condition": "$(OP_INPUTINDEX OP_UTXOTOKENAMOUNT <0> OP_GREATERTHAN)",
"transaction": "burnFungibleTokensTransaction"
},
"burnNonfungibleTokens": {
"name": "Delete a Non-fungible Token",
"description": "Permanently and irreversibly deletes one non-fungible token.",
"icon": "burn",
"roles": {
"owner": {
"requirements": {
"variables": [
"burnedTokenCategory",
"burnedTokenCapability",
"burnedTokenCommitment"
],
"secrets": ["ownerKey"]
}
}
},
"requirements": {
"roles": [{ "role": "owner", "slots": { "min": 1, "max": 1 } }]
},
"condition": "$(OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_SIZE OP_NIP <32> OP_GREATERTHAN)",
"transaction": "burnNonfungibleTokenTransaction"
},
"sign": {
"name": "Sign Message",
"description": "Signs a provided message using the Bitcoin message signing protocol.",
"icon": "sign",
"roles": {
"owner": {
"requirements": {
"variables": ["messageToSign"],
"secrets": ["ownerKey"]
}
}
},
"requirements": {
"roles": [{ "role": "owner", "slots": { "min": 1, "max": 1 } }]
},
"data": ["messageSignature"]
},
"verify": {
"name": "Verify Message Signature",
"description": "Verifies a provided message signature according to the Bitcoin message signing protocol.",
"icon": "verify",
"roles": {
"owner": {
"requirements": {
"variables": ["messageSignature", "messageToVerify"],
"secrets": ["ownerKey"]
}
}
},
"requirements": {
"roles": [{ "role": "owner", "slots": { "min": 1, "max": 1 } }]
},
"data": ["messageSignatureValidity"]
}
},
"data": {
"messageSignature": {
"value": "$(<messagePrefix> <message> OP_CAT <ownerKey.data_signature.top_stack_element>)",
"type": "bytes",
"hint": "signature"
},
"messageSignatureValidity": {
"value": "$(<messageSignature> <messagePrefix> <message> OP_CAT <ownerKey.public_key> OP_CHECKDATASIG)",
"type": "integer",
"hint": "script_boolean"
}
},
"transactions": {
"receiveTransaction": {
"name": "Transfer request",
"description": "Transfer request for an unspecified amount of cash and/or tokens.",
"icon": "request",
"roles": {
"receiver": {
"name": "Received",
"description": "Received an unspecified amount of cash and/or tokens.",
"icon": "receive"
},
"sender": {
"name": "Sent",
"description": "Sent an unspecified amount of cash and/or tokens.",
"icon": "send"
}
},
"inputs": [],
"outputs": [{ "output": "receiveOutput" }],
"version": 2,
"locktime": 0,
"composable": true
},
"requestSatoshisTransaction": {
"name": "Transfer request",
"description": "Transfer request for $(<requestedSatoshis>) satoshis.",
"icon": "request",
"roles": {
"receiver": {
"name": "Received",
"description": "Received $(<requestedSatoshis>) satoshis.",
"icon": "receive"
},
"sender": {
"name": "Sent",
"description": "Sent $(<requestedSatoshis>) satoshis.",
"icon": "send"
}
},
"inputs": [],
"outputs": [{ "output": "requestSatoshisOutput" }],
"version": 2,
"locktime": 0,
"composable": true
},
"requestFungibleTokensTransaction": {
"name": "Transfer request",
"description": "Transfer request for $(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_DIV).$(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_MOD) $(<requestedTokenCategory.symbol>) tokens.",
"icon": "request",
"roles": {
"receiver": {
"name": "Received",
"description": "Received $(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_DIV).$(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_MOD) $(<requestedTokenCategory.symbol>) tokens.",
"icon": "receive"
},
"sender": {
"name": "Sent",
"description": "Sent $(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_DIV).$(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_MOD) $(<requestedTokenCategory.symbol>) tokens.",
"icon": "send"
}
},
"inputs": [],
"outputs": [{ "output": "requestFungibleTokensOutput" }],
"version": 2,
"locktime": 0,
"composable": true
},
"requestNonfungibleTokensTransaction": {
"name": "Transfer request",
"description": "Transfer request for one non-fungible $(<requestedTokenCapability> <0x02> OP_EQUAL OP_IF <\"minting\"> OP_ELSE <requestedTokenCapability> <0x01> OP_EQUAL OP_IF <\"mutable\"> OP_ELSE <\"immutable\"> OP_ENDIF OP_ENDIF) (<requestedTokenCategory.symbol>) token, with $(<requestedTokenCommitment>) commitment.",
"icon": "request",
"roles": {
"receiver": {
"name": "Received",
"description": "Received one non-fungible $(<requestedTokenCapability> <0x02> OP_EQUAL OP_IF <\"minting\"> OP_ELSE <requestedTokenCapability> <0x01> OP_EQUAL OP_IF <\"mutable\"> OP_ELSE <\"immutable\"> OP_ENDIF OP_ENDIF) (<requestedTokenCategory.symbol>) token, with $(<requestedTokenCommitment>) commitment.",
"icon": "receive"
},
"sender": {
"name": "Sent",
"description": "Sent the requested non-fungible $(<requestedTokenCapability> <0x02> OP_EQUAL OP_IF <\"minting\"> OP_ELSE <requestedTokenCapability> <0x01> OP_EQUAL OP_IF <\"mutable\"> OP_ELSE <\"immutable\"> OP_ENDIF OP_ENDIF) (<requestedTokenCategory.symbol>) token, with $(<requestedTokenCommitment>) commitment.",
"icon": "send"
}
},
"inputs": [],
"outputs": [{ "output": "requestNonfungibleTokensOutput" }],
"version": 2,
"locktime": 0,
"composable": true
},
"transferSatoshisTransaction": {
"name": "Satoshis Transferred",
"description": "$(<transferredSatoshis>) satoshis were transferred to a recipient.",
"icon": "send",
"roles": {
"receiver": {
"name": "Received",
"description": "Received $(<transferredSatoshis>) satoshis.",
"icon": "receive"
},
"sender": {
"name": "Sent",
"description": "Sent $(<transferredSatoshis>) satoshis.",
"icon": "send"
}
},
"inputs": [],
"outputs": [{ "output": "transferSatoshisOutput" }],
"version": 2,
"locktime": 0,
"composable": true
},
"transferFungibleTokensTransaction": {
"name": "Fungible Tokens Transferred",
"description": "$(<transferredTokenAmount> <transferredTokenCategory.decimalsFactor> OP_DIV).$(<transferredTokenAmount> <transferredTokenCategory.decimalsFactor> OP_MOD) $(<transferredTokenCategory.symbol>) tokens were transferred to a recipient.",
"icon": "send",
"roles": {
"receiver": {
"name": "Received",
"description": "Received $(<transferredTokenAmount> <transferredTokenCategory.decimalsFactor> OP_DIV).$(<transferredTokenAmount> <transferredTokenCategory.decimalsFactor> OP_MOD) $(<transferredTokenCategory.symbol>) tokens.",
"icon": "receive"
},
"sender": {
"name": "Sent",
"description": "Sent $(<transferredTokenAmount> <transferredTokenCategory.decimalsFactor> OP_DIV).$(<transferredTokenAmount> <transferredTokenCategory.decimalsFactor> OP_MOD) $(<transferredTokenCategory.symbol>) tokens.",
"icon": "send"
}
},
"inputs": [],
"outputs": [{ "output": "transferFungibleTokensOutput" }],
"version": 2,
"locktime": 0,
"composable": true
},
"transferNonfungibleTokensTransaction": {
"name": "Non-fungible Token Transferred",
"description": "One non-fungible $(<transferredTokenCapability> <0x02> OP_EQUAL OP_IF <\"minting\"> OP_ELSE <transferredTokenCapability> <0x01> OP_EQUAL OP_IF <\"mutable\"> OP_ELSE <\"immutable\"> OP_ENDIF OP_ENDIF) $(<transferredTokenCategory.symbol>) token was transferred to a recipient, with $(<transferredTokenCommitment>) commitment.",
"icon": "send",
"roles": {
"receiver": {
"name": "Received",
"description": "Received one non-fungible $(<transferredTokenCapability> <0x02> OP_EQUAL OP_IF <\"minting\"> OP_ELSE <transferredTokenCapability> <0x01> OP_EQUAL OP_IF <\"mutable\"> OP_ELSE <\"immutable\"> OP_ENDIF OP_ENDIF) (<transferredTokenCategory.symbol>) token, with $(<transferredTokenCommitment>) commitment.",
"icon": "receive"
},
"sender": {
"name": "Sent",
"description": "Sent one non-fungible $(<transferredTokenCapability> <0x02> OP_EQUAL OP_IF <\"minting\"> OP_ELSE <transferredTokenCapability> <0x01> OP_EQUAL OP_IF <\"mutable\"> OP_ELSE <\"immutable\"> OP_ENDIF OP_ENDIF) (<transferredTokenCategory.symbol>) token, with $(<transferredTokenCommitment>) commitment.",
"icon": "send"
}
},
"inputs": [],
"outputs": [{ "output": "transferNonfungibleTokenOutput" }],
"version": 2,
"locktime": 0,
"composable": true
},
"burnFungibleTokensTransaction": {
"name": "Deleted fungible tokens",
"description": "Permanently and irreversibly deleted $(<burnedTokenAmount> <burnedTokenCategory.decimalsFactor> OP_DIV).$(<burnedTokenAmount> <burnedTokenCategory.decimalsFactor> OP_MOD) $(<burnedTokenCategory.symbol>) tokens.",
"icon": "burn",
"inputs": [{ "input": "burnFungibleTokensInput" }],
"outputs": [],
"version": 2,
"locktime": 0,
"composable": true
},
"burnNonfungibleTokenTransaction": {
"name": "Deleted non fungible token",
"description": "Permanently and irreversibly deleted a non-fungible $(<burnedTokenCapability> <0x02> OP_EQUAL OP_IF <\"minting\"> OP_ELSE <burnedTokenCapability> <0x01> OP_EQUAL OP_IF <\"mutable\"> OP_ELSE <\"immutable\"> OP_ENDIF OP_ENDIF) (<burnedTokenCategory.symbol>) token, with $(<burnedTokenCommitment>) commitment.",
"icon": "burn",
"inputs": [{ "input": "burnNonfungibleTokenInput" }],
"outputs": [],
"version": 2,
"locktime": 0,
"composable": true
}
},
"outputs": {
"receiveOutput": {
"name": "Recipient output",
"description": "Transferred an unspecified amount of cash and/or tokens to a recipient.",
"icon": "receive",
"roles": {
"receiver": {
"name": "Received",
"description": "Received an unspecified amount of cash and/or tokens."
},
"sender": {
"name": "Sent",
"description": "Sent an unspecified amount of cash and/or tokens."
}
},
"lockscript": "receivingLockingScript"
},
"requestSatoshisOutput": {
"name": "Recipient output",
"description": "Transferred $(<requestedSatoshis>) satoshis to a recipient.",
"icon": "request",
"roles": {
"receiver": {
"name": "Received",
"description": "Received $(<requestedSatoshis>) satoshis."
},
"sender": {
"name": "Sent",
"description": "Sent $(<requestedSatoshis>) satoshis."
}
},
"lockscript": "receivingLockingScript",
"valueSatoshis": "$(<requestedSatoshis>)",
"token": null
},
"requestFungibleTokensOutput": {
"name": "Recipient output",
"description": "Transferred $(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_DIV).$(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_MOD) $(<requestedTokenCategory.symbol>) tokens to a recipient.",
"icon": "request",
"roles": {
"receiver": {
"name": "Received",
"description": "Received $(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_DIV).$(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_MOD) $(<requestedTokenCategory.symbol>) tokens."
},
"sender": {
"name": "Sent",
"description": "Sent $(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_DIV).$(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_MOD) $(<requestedTokenCategory.symbol>) tokens."
}
},
"lockscript": "receivingLockingScript",
"valueSatoshis": "1000",
"token": {
"category": "$(<requestedTokenCategory>)",
"amount": "$(<requestedTokenAmount>)",
"nft": null
}
},
"requestNonfungibleTokensOutput": {
"name": "Recipient output",
"description": "Transferred one non-fungible $(<requestedTokenCapability> <0x02> OP_EQUAL OP_IF <\"minting\"> OP_ELSE <requestedTokenCapability> <0x01> OP_EQUAL OP_IF <\"mutable\"> OP_ELSE <\"immutable\"> OP_ENDIF OP_ENDIF) $(<requestedTokenCategory.symbol>) token to a recipient, with $(<requestedTokenCommitment>) commitment.",
"icon": "request",
"roles": {
"receiver": {
"name": "Received",
"description": "Received one non-fungible $(<requestedTokenCapability> <0x02> OP_EQUAL OP_IF <\"minting\"> OP_ELSE <requestedTokenCapability> <0x01> OP_EQUAL OP_IF <\"mutable\"> OP_ELSE <\"immutable\"> OP_ENDIF OP_ENDIF) $(<requestedTokenCategory.symbol>) token, with $(<requestedTokenCommitment>) commitment."
},
"sender": {
"name": "Sent",
"description": "Sent the requested non-fungible $(<requestedTokenCapability> <0x02> OP_EQUAL OP_IF <\"minting\"> OP_ELSE <requestedTokenCapability> <0x01> OP_EQUAL OP_IF <\"mutable\"> OP_ELSE <\"immutable\"> OP_ENDIF OP_ENDIF) $(<requestedTokenCategory.symbol>) token, with $(<requestedTokenCommitment>) commitment."
}
},
"lockscript": "receivingLockingScript",
"valueSatoshis": "1000",
"token": {
"category": "$(<requestedTokenCategory>)",
"amount": null,
"nft": {
"capability": "$(<requestedTokenCapability>)",
"commitment": "$(<requestedTokenCommitment>)"
}
}
},
"transferSatoshisOutput": {
"name": "Recipient output",
"description": "Transferred $(<transferredSatoshis>) satoshis to a recipient.",
"icon": "send",
"roles": {
"receiver": {
"name": "Received",
"description": "Received $(<transferredSatoshis>) satoshis."
},
"sender": {
"name": "Sent",
"description": "Sent $(<transferredSatoshis>) satoshis."
}
},
"lockscript": "sendingLockingscript",
"valueSatoshis": "$(<transferredSatoshis>)"
},
"transferFungibleTokensOutput": {
"name": "Recipient output",
"description": "Transferred $(<transferredTokenAmount> <transferredTokenCategory.decimalsFactor> OP_DIV).$(<transferredTokenAmount> <transferredTokenCategory.decimalsFactor> OP_MOD) $(<transferredTokenCategory.symbol>) tokens to a recipient.",
"icon": "send",
"roles": {
"receiver": {
"name": "Received",
"description": "Received $(<transferredTokenAmount> <transferredTokenCategory.decimalsFactor> OP_DIV).$(<transferredTokenAmount> <transferredTokenCategory.decimalsFactor> OP_MOD) $(<transferredTokenCategory.symbol>) tokens."
},
"sender": {
"name": "Sent",
"description": "Sent $(<transferredTokenAmount> <transferredTokenCategory.decimalsFactor> OP_DIV).$(<transferredTokenAmount> <transferredTokenCategory.decimalsFactor> OP_MOD) $(<transferredTokenCategory.symbol>) tokens."
}
},
"lockscript": "sendingLockingscript",
"token": {
"category": "$(<transferredTokenCategory>)",
"amount": "$(<transferredTokenAmount>)"
}
},
"transferNonfungibleTokenOutput": {
"name": "Recipient output",
"description": "Transferred one non-fungible $(<transferredTokenCapability> <0x02> OP_EQUAL OP_IF <\"minting\"> OP_ELSE <transferredTokenCapability> <0x01> OP_EQUAL OP_IF <\"mutable\"> OP_ELSE <\"immutable\"> OP_ENDIF OP_ENDIF) $(<transferredTokenCategory.symbol>) token to a recipient, with $(<transferredTokenCommitment>) commitment.",
"icon": "send",
"roles": {
"receiver": {
"name": "Received",
"description": "Received one non-fungible $(<transferredTokenCapability> <0x02> OP_EQUAL OP_IF <\"minting\"> OP_ELSE <transferredTokenCapability> <0x01> OP_EQUAL OP_IF <\"mutable\"> OP_ELSE <\"immutable\"> OP_ENDIF OP_ENDIF) $(<transferredTokenCategory.symbol>) token, with $(<transferredTokenCommitment>) commitment."
},
"sender": {
"name": "Sent",
"description": "Sent one non-fungible $(<transferredTokenCapability> <0x02> OP_EQUAL OP_IF <\"minting\"> OP_ELSE <transferredTokenCapability> <0x01> OP_EQUAL OP_IF <\"mutable\"> OP_ELSE <\"immutable\"> OP_ENDIF OP_ENDIF) $(<transferredTokenCategory.symbol>) token, with $(<transferredTokenCommitment>) commitment."
}
},
"lockscript": "sendingLockingscript",
"token": {
"category": "$(<transferredTokenCategory>)",
"nft": {
"capability": "$(<transferredTokenCapability>)",
"commitment": "$(<transferredTokenCommitment>)"
}
}
}
},
"inputs": {
"burnFungibleTokensInput": {
"name": "Deleted fungible tokens",
"description": "Permanently and irreversibly deleted $(<burnedTokenAmount>) $(<burnedTokenCategory>).",
"icon": "burn",
"unlockingScript": "unlockP2PKH",
"token": {
"category": "$(<burnedTokenCategory>)",
"amount": "$(<this.fungibleTokenAmount> <burnedTokenAmount> OP_GREATERTHANOREQUAL OP_IF <this.fungibleTokens> OP_ENDIF)"
},
"omitChangeAmounts": { "fungibleTokens": "${<burnedTokenAmount>}" }
},
"burnNonfungibleTokenInput": {
"name": "Deleted non-fungible token",
"description": "Permanently and irreversibly burned one non-fungible $(<burnedTokenCapability> <0x02> OP_EQUAL OP_IF <\"minting\"> OP_ELSE <burnedTokenCapability> <0x01> OP_EQUAL OP_IF <\"mutable\"> OP_ELSE <\"immutable\"> OP_ENDIF OP_ENDIF) token of category $(<burnedTokenCategory>), with a $(<burnedTokenCommitment>) commitment.",
"icon": "burn",
"unlockingScript": "unlockP2PKH",
"token": {
"category": "$(<burnedTokenCategory>)",
"nft": {
"capability": "$(<burnedTokenCapability>)",
"commitment": "$(<burnedTokenCommitment>)"
}
},
"omitChangeAmounts": { "nonfungibleTokens": 1 }
}
},
"lockingScripts": {
"sendingLockingscript": {
"name": "Sent",
"description": "Funds sent to an external recipient",
"icon": "address",
"lockingType": "standard",
"lockingScript": "lockToRecipient",
"actions": [],
"state": [],
"secrets": [],
"balance": false,
"selectable": false,
"privacy": false
},
"receivingLockingScript": {
"name": "Received",
"description": "Funds received without wallet coordination.",
"icon": "address",
"lockingType": "standard",
"lockingScript": "lockP2PKH",
"unlockingScript": "unlockP2PKH",
"roles": {
"receiver": {
"state": { "variables": [], "secrets": ["ownerKey"] },
"actions": [
{ "action": "sign", "role": "owner", "secrets": ["ownerKey"] },
{ "action": "verify", "role": "owner", "secrets": ["ownerKey"] },
{
"action": "sendSatoshis",
"role": "sender",
"secrets": ["ownerKey"]
},
{
"action": "sendFungibleTokens",
"role": "sender",
"secrets": ["ownerKey"]
},
{
"action": "sendNonfungibleTokens",
"role": "sender",
"secrets": ["ownerKey"]
},
{
"action": "burnFungibleTokens",
"role": "sender",
"secrets": ["ownerKey"]
},
{
"action": "burnNonfungibleTokens",
"role": "sender",
"secrets": ["ownerKey"]
}
],
"balance": { "satoshis": true, "fungibleTokens": true },
"selectable": true,
"privacy": false
}
}
}
},
"scripts": {
"lockP2PKH": "OP_DUP OP_HASH160 <$(<ownerKey.public_key> OP_HASH160)> OP_EQUALVERIFY OP_CHECKSIG",
"unlockP2PKH": "<ownerKey.schnorr_signature.all_outputs> <ownerKey.public_key>",
"lockToRecipient": "<recipientLockingscript>"
},
"constants": {
"dustLimit": {
"name": "Dust Limit",
"description": "Standard required minimum satoshis for Pay to Public Key Hash outputs.",
"type": "integer",
"value": 546
},
"messagePrefix": {
"name": "Message Prefix",
"description": "Standard message prefix used in the bitcoin signed message protocol.",
"type": "bytes",
"value": "0x18426974636f696e205369676e6564204d6573736167653a0a"
}
},
"variables": {
"ownerKey": {
"name": "Owners Private Key",
"description": "The private key used to authorize spending of received funds.",
"type": "bytes",
"hint": "private_key"
},
"messageToSign": {
"name": "Message",
"description": "The text message to sign.",
"type": "string"
},
"messageToVerify": {
"name": "Message",
"description": "The text message to verify.",
"type": "string"
},
"messageSignature": {
"name": "Message Signature",
"description": "The signature for the message.",
"type": "bytes",
"hint": "signature"
},
"requestedSatoshis": {
"name": "Requested Amount",
"description": "The Bitcoin Cash amount requested",
"type": "integer",
"hint": "satoshis"
},
"requestedTokenCategory": {
"name": "Requested Token Category",
"description": "The token category requested",
"type": "bytes",
"hint": "token_category"
},
"requestedTokenAmount": {
"name": "Requested Token Amount",
"description": "The fungible token amount requested",
"type": "integer",
"hint": "token_amount"
},
"requestedTokenCapability": {
"name": "Requested Token Capability",
"description": "The non-fungible token capability requested",
"type": "bytes",
"hint": "token_capability"
},
"requestedTokenCommitment": {
"name": "Requested Token Commitment",
"description": "The non-fungible token commitment requested",
"type": "bytes",
"hint": "token_commitment"
},
"transferredTokenCategory": {
"name": "Sending Token Category",
"description": "The token category of the token(s) to send",
"type": "bytes",
"hint": "token_category"
},
"transferredTokenAmount": {
"name": "Sending Token Amount",
"description": "The fungible token amount to send",
"type": "integer",
"hint": "token_amount"
},
"transferredTokenCapability": {
"name": "Sending Token Capability",
"description": "The token capability for the non-fungible token to send",
"type": "bytes",
"hint": "token_capability"
},
"transferredTokenCommitment": {
"name": "Sending Token Commitment",
"description": "The token commitment for the non-fungible token to send",
"type": "bytes",
"hint": "token_commitment"
},
"burnedTokenCategory": {
"name": "Deleted Token Category",
"description": "The token category of the token(s) to delete",
"type": "bytes",
"hint": "token_category"
},
"burnedTokenAmount": {
"name": "Deleted Token Amount",
"description": "The fungible token amount to delete",
"type": "integer",
"hint": "token_amount"
},
"burnedTokenCapability": {
"name": "Deleted Token Capability",
"description": "The token capability for the non-fungible token to delete",
"type": "bytes",
"hint": "token_capability"
},
"burnedTokenCommitment": {
"name": "Deleted Token Commitment",
"description": "The token commitment for the non-fungible token to delete",
"type": "bytes",
"hint": "token_commitment"
}
},
"icons": [
{ "name": "wallet", "hash": "0000000000000000000000" },
{ "name": "owner", "hash": "0000000000000000000000" },
{ "name": "sender", "hash": "0000000000000000000000" },
{ "name": "address", "hash": "0000000000000000000000" },
{ "name": "receive", "hash": "0000000000000000000000" },
{ "name": "request", "hash": "0000000000000000000000" },
{ "name": "send", "hash": "0000000000000000000000" },
{ "name": "burn", "hash": "0000000000000000000000" },
{ "name": "sign", "hash": "0000000000000000000000" },
{ "name": "verify", "hash": "0000000000000000000000" }
],
"scenarios": [
{
"name": "requesting satoshis",
"description": "happy-path evaluation for requesting satoshis.",
"action": "requestSatoshis",
"roles": [
{
"role": "receiver",
"values": {
"generated": {
"ownerKey": "KyRQa5pEXuzVcDwnXRLpYAascjchQW5DoxVRMbj4DTxS83573mz8"
},
"variables": { "requestedSatoshis": 2000 },
"secrets": {},
"inputs": [],
"outputs": [
{
"lockingBytecode": "76a91475c715ecb74178fe87933e57e947e5e92d904b8188ac",
"valueSatoshis": 2000
}
]
}
},
{
"role": "sender",
"values": {
"generated": {},
"variables": {},
"secrets": {},
"inputs": [
{
"outpointTransactionHash": "4ef28553a31a266719e66ba97fee3aeecd6d1788f7ff6ab12f8ebceda49660c0",
"outpointIndex": 0,
"sequenceNumber": 0,
"unlockingBytecode": "41226b2be7c2890c8bbde2f79e79640e56d866843f2e822ec51c469019d13db04a422c9ee49f5eefd26fee24e91910edbbb032b90cc54c34da80a61e69b0ee3d22412103e7ab26c36a7c7f45b2c26f33c08b0fa43a633268700f47216646d4cb37ae5696"
}
],
"outputs": [
{
"lockingBytecode": "76a91475c715ecb74178fe87933e57e947e5e92d904b8188ac",
"valueSatoshis": 2000
}
]
}
}
],
"values": { "generated": {}, "variables": {}, "secrets": {} },
"outcome": {
"roles": {
"receiver": {
"name": "Request",
"description": "Requested a specific amount of satoshis from one or more senders.",
"icon": "request"
},
"sender": {
"name": "Send",
"description": "Sent a specific amount of satoshis to the provided receiver.",
"icon": "send"
}
},
"transactions": [
{
"transaction": "0200000001c06096a4edbc8e2fb16afff788176dcdee3aee7fa96be61967261aa35385f24e000000006441226b2be7c2890c8bbde2f79e79640e56d866843f2e822ec51c469019d13db04a422c9ee49f5eefd26fee24e91910edbbb032b90cc54c34da80a61e69b0ee3d22412103e7ab26c36a7c7f45b2c26f33c08b0fa43a633268700f47216646d4cb37ae5696000000000267530300000000001976a91475c715ecb74178fe87933e57e947e5e92d904b8188acd0070000000000001976a91475c715ecb74178fe87933e57e947e5e92d904b8188ac00000000",
"value": ""
}
]
}
}
]
}

4330
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -38,8 +38,8 @@
"@generalprotocols/oracle-client": "^0.0.1-development.11945476152", "@generalprotocols/oracle-client": "^0.0.1-development.11945476152",
"@xo-cash/crypto": "^0.0.1", "@xo-cash/crypto": "^0.0.1",
"@xo-cash/engine": "file:../engine", "@xo-cash/engine": "file:../engine",
"@xo-cash/state": "0.0.1", "@xo-cash/state": "file:../state",
"@xo-cash/templates": "^0.0.3", "@xo-cash/templates": "file:../templates",
"@xo-cash/types": "^0.0.1", "@xo-cash/types": "^0.0.1",
"better-sqlite3": "^12.6.2", "better-sqlite3": "^12.6.2",
"clipboardy": "^5.1.0", "clipboardy": "^5.1.0",

View File

@@ -9,7 +9,7 @@ mkdir xo-terminal && cd xo-terminal
# ----- Start Engine Setup ----- # ----- Start Engine Setup -----
# Clone the Engine Repo (Note, this uses harvey's fork of the engine repo to access the cli-test branch) # Clone the Engine Repo (Note, this uses harvey's fork of the engine repo to access the cli-test branch)
git clone https://gitlab.com/Harvmaster/engine.git git clone git@gitlab.com:Harvmaster/engine.git
# Move into teh engine directory # Move into teh engine directory
cd engine cd engine
@@ -27,9 +27,28 @@ npm run build
# Move back to the top level directory # Move back to the top level directory
cd .. cd ..
# ----- Start State Setup -----
# Clone the State Repo
git clone git@gitlab.com:Harvmaster/state.git
# Move into the state directory
cd state
git checkout in-memory-adapter
# Install the dependencies
npm ci
# Build the state
npm run build
# ----- End State Setup -----
# Move back to the top level directory
cd ..
# ----- Start CLI Setup ----- # ----- Start CLI Setup -----
# Clone the CLI Repo # Clone the CLI Repo
git clone https://git.harvmaster.com/Harvmaster/xo-cli.git git clone git@git.harvmaster.com:Harvmaster/xo-cli.git
# Move into the cli directory # Move into the cli directory
cd xo-cli cd xo-cli

View File

@@ -57,7 +57,7 @@ npx tsx src/index.ts # TUI
xo-cli mnemonic create xo-cli mnemonic create
# Import an existing mnemonic seed phrase # Import an existing mnemonic seed phrase
xo-cli mnemonic import oven crop same above under tower promote decrease vocal pretty require slow xo-cli mnemonic import page pencil stock planet limb cluster assault speak off joke private pioneer
# List mnemonic basenames (use with -m) # List mnemonic basenames (use with -m)
xo-cli mnemonic list xo-cli mnemonic list

View File

@@ -7,9 +7,9 @@ import { z } from "zod";
/** /**
* Converts the CLI args to a key-value object and return the options object along with the other arguments still in the array.\ * Converts the CLI args to a key-value object and return the options object along with the other arguments still in the array.\
* eg: `xo-cli mnemonic create oven crop same above under tower promote decrease vocal pretty require slow -v -o mnemonic.txt` will return: * eg: `xo-cli mnemonic create page pencil stock planet limb cluster assault speak off joke private pioneer -v -o mnemonic.txt` will return:
* { * {
* args: ["mnemonic", "create", "oven", "crop", "same", "above", "under", "tower", "promote", "decrease", "vocal", "pretty", "require", "slow"], * args: ["mnemonic", "create", "page", "pencil", "stock", "planet", "limb", "cluster", "assault", "speak", "off", "joke", "private", "pioneer"],
* options: { * options: {
* output: "mnemonic.txt", * output: "mnemonic.txt",
* verbose: "true", * verbose: "true",

View File

@@ -510,7 +510,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
const templates = await this.engine.listImportedTemplates(); const templates = await this.engine.listImportedTemplates();
// For each template, we need to create a 2d array of all the outputs // For each template, we need to create a 2d array of all the outputs
const outputs = templates.map(template => { const outputs = templates.flatMap(template => {
return Object.keys(template.outputs).map(output => { return Object.keys(template.outputs).map(output => {
const templateIdentifier = generateTemplateIdentifier(template); const templateIdentifier = generateTemplateIdentifier(template);
@@ -522,7 +522,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
}); });
// then, for each output, we need to get the spendable resources // then, for each output, we need to get the spendable resources
const spendableResources = await Promise.all(outputs.flat().map(output => { const spendableResources = await Promise.all(outputs.map(output => {
return this.engine.getSpendableResources(this.data, { return this.engine.getSpendableResources(this.data, {
templateIdentifier: output.templateIdentifier, templateIdentifier: output.templateIdentifier,
outputIdentifier: output.outputIdentifier, outputIdentifier: output.outputIdentifier,

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
*/ */
import type { Invitation } from "../services/invitation.js"; import type { Invitation } from "../services/invitation.js";
import type { XOTemplate } from "@xo-cash/types"; import type { XOInvitationCommit, XOTemplate } from "@xo-cash/types";
/** /**
* Color names for invitation states. * Color names for invitation states.
@@ -249,9 +249,9 @@ export function formatInvitationId(id: string, maxLength: number = 16): string {
* @param invitation - The invitation to check * @param invitation - The invitation to check
* @returns Array of unique entity identifiers * @returns Array of unique entity identifiers
*/ */
export function getInvitationParticipants(invitation: Invitation): string[] { export function getInvitationParticipants(commits: Array<XOInvitationCommit>): string[] {
const participants = new Set<string>(); const participants = new Set<string>();
for (const commit of invitation.data.commits || []) { for (const commit of commits) {
if (commit.entityIdentifier) { if (commit.entityIdentifier) {
participants.add(commit.entityIdentifier); participants.add(commit.entityIdentifier);
} }
@@ -267,9 +267,14 @@ export function getInvitationParticipants(invitation: Invitation): string[] {
* @returns True if the user has made at least one commit * @returns True if the user has made at least one commit
*/ */
export function isUserParticipant( export function isUserParticipant(
invitation: Invitation, invitation: Invitation | Array<XOInvitationCommit>,
userEntityId: string | null, userEntityId: string | null,
): boolean { ): boolean {
if (!userEntityId) return false; if (!userEntityId) return false;
return getInvitationParticipants(invitation).includes(userEntityId);
if (Array.isArray(invitation)) {
return invitation.some(commit => commit.entityIdentifier === userEntityId);
}
return getInvitationParticipants(invitation.data.commits).includes(userEntityId);
} }

View File

@@ -19,7 +19,8 @@ import {
import { BCHMnemonicURL } from "../../src/utils/bch-mnemonic-url"; import { BCHMnemonicURL } from "../../src/utils/bch-mnemonic-url";
const TEST_SEED = const TEST_SEED =
"oven crop same above under tower promote decrease vocal pretty require slow"; "page pencil stock planet limb cluster assault speak off joke private pioneer";
describe("mnemonic utilities", () => { describe("mnemonic utilities", () => {
let tempDir: string; let tempDir: string;
@@ -53,7 +54,7 @@ describe("mnemonic utilities", () => {
test("creates a mnemonic file with auto-generated name", () => { test("creates a mnemonic file with auto-generated name", () => {
const filename = createMnemonicFile(tempDir, TEST_SEED); const filename = createMnemonicFile(tempDir, TEST_SEED);
expect(filename).toMatch(/^mnemonic-oven$/); expect(filename).toMatch(/^mnemonic-page$/);
expect(existsSync(path.join(tempDir, filename))).toBe(true); expect(existsSync(path.join(tempDir, filename))).toBe(true);
}); });

View File

@@ -1,10 +1,9 @@
// Node js tool for temp dir
import { tmpdir } from "node:os";
import { BlockchainMonitor, Engine } from "@xo-cash/engine"; import { BlockchainMonitor, Engine } from "@xo-cash/engine";
import { import {
createStorageAdapter,
State, State,
StorageType,
UnspentOutputStatus, UnspentOutputStatus,
type UnspentOutputData, type UnspentOutputData,
} from "@xo-cash/state"; } from "@xo-cash/state";
@@ -13,15 +12,13 @@ import { convertMnemonicToSeedBytes } from "@xo-cash/crypto";
import { binToHex, sha256 } from "@bitauth/libauth"; import { binToHex, sha256 } from "@bitauth/libauth";
import { AppService } from "../../../src/services/app"; import { AppService } from "../../../src/services/app";
import { InMemoryStorage as XOStorageInMemory } from './xo-storage-in-memory'
import { InMemoryStorage } from "../../../src/services/storage"; import { InMemoryStorage } from "../../../src/services/storage";
import { MockElectrumService } from "./electrum-service"; import { MockElectrumService } from "./electrum-service";
import { MockRatesService } from "./rates-service"; import { MockRatesService } from "./rates-service";
import { RatesService } from "../../../src/services/rates"; import { RatesService } from "../../../src/services/rates";
import { SettingsService } from "../../../src/services/settings";
export const DEFAULT_SEED = export const DEFAULT_SEED =
"oven crop same above under tower promote decrease vocal pretty require slow"; "page pencil stock planet limb cluster assault speak off joke private pioneer";
/** /**
* Options for creating a fake resource (UTXO) in tests. * Options for creating a fake resource (UTXO) in tests.
@@ -70,6 +67,8 @@ export const addFakeResource = async (
status: UnspentOutputStatus.CONFIRMED, status: UnspentOutputStatus.CONFIRMED,
selectable: true, selectable: true,
privacy: false, privacy: false,
templateIdentifier: options.templateIdentifier ?? "test-template",
outputIdentifier: options.outputIdentifier ?? "receiveOutput",
outpointIndex: options.outpointIndex ?? 0, outpointIndex: options.outpointIndex ?? 0,
outpointTransactionHash: options.outpointTransactionHash ?? randomTxHash(), outpointTransactionHash: options.outpointTransactionHash ?? randomTxHash(),
minedAtHeight: options.minedAtHeight ?? 800000, minedAtHeight: options.minedAtHeight ?? 800000,
@@ -131,7 +130,10 @@ export const unreserveResource = async (
*/ */
export const createMockEngine = async (seed: string) => { export const createMockEngine = async (seed: string) => {
// Create the in-memory storage adapter. // Create the in-memory storage adapter.
const storage = new XOStorageInMemory(); const storage = await createStorageAdapter({
storageType: "inmemory",
accountHash: binToHex(sha256.hash(convertMnemonicToSeedBytes(seed))),
});
// Initialize the storage adapter. // Initialize the storage adapter.
await storage.initialize(); await storage.initialize();
@@ -141,7 +143,10 @@ export const createMockEngine = async (seed: string) => {
// Create the in-memory blockchain provider. // Create the in-memory blockchain provider.
const blockchainProvider = new InMemoryBlockchainProvider(); const blockchainProvider = new InMemoryBlockchainProvider();
await blockchainProvider.initialize(); await blockchainProvider.initialize({
applicationIdentifier: "xo-cli-tests",
electrumOptions: {},
});
// Create the blockchain monitor instance. // Create the blockchain monitor instance.
const blockchainMonitor = new BlockchainMonitor(state, blockchainProvider); const blockchainMonitor = new BlockchainMonitor(state, blockchainProvider);
@@ -155,15 +160,10 @@ export const createMockEngine = async (seed: string) => {
}; };
export const createMockAppService = async (engine: Engine) => { export const createMockAppService = async (engine: Engine) => {
const settings = new SettingsService(
`${tmpdir()}/xo-cli-tests-settings.json`,
);
settings.setCurrency("USD");
const storage = await InMemoryStorage.create(); const storage = await InMemoryStorage.create();
const mockRates = new MockRatesService(); const mockRates = new MockRatesService();
const rates = new RatesService(mockRates, settings); const rates = new RatesService(mockRates);
const mockElectrum = new MockElectrumService(); const mockElectrum = new MockElectrumService();
@@ -176,5 +176,5 @@ export const createMockAppService = async (engine: Engine) => {
invitationStoragePath: "test-invitations.db", invitationStoragePath: "test-invitations.db",
}; };
return new AppService(engine, storage, config, mockElectrum, rates, settings); return new AppService(engine, storage, config, mockElectrum, rates);
}; };

View File

@@ -1,536 +0,0 @@
import type { XOInvitation, XOTemplate } from '@xo-cash/types';
import {
DatabaseClosedError,
DatabaseNotInitializedError,
QueryFieldsLimitExceededError,
QueryInvalidError,
UnspentOutputNotFoundInStoreError,
UnspentOutputReservationConflictError,
DatabaseState,
EventData,
EventDataQuery,
ScriptHashData,
ScriptHashDataQuery,
UnspentOutputData,
UnspentOutputDataFilter,
UnspentOutputDataQuery,
UnspentOutputStatus,
XOStorage,
} from '@xo-cash/state';
import { generateTemplateIdentifier } from '@xo-cash/engine';
/**
* Returns the names of all fields in a query object that have a defined value.
*
* @param query - The query object to inspect
* @returns Array of field names whose values are not undefined
*/
export const getDefinedQueryFields = (query: Record<string, unknown>): string[] => {
const definedFields: string[] = [];
for (const [ field, value ] of Object.entries(query)) {
if (value !== undefined) {
definedFields.push(field);
}
}
return definedFields;
};
/**
* In-memory storage adapter implementation.
*
* This adapter mirrors the behavior of the IndexedDB implementation while keeping
* all data in process memory. It is useful for tests and ephemeral sessions where
* persistence across process restarts is not required.
*/
export class InMemoryStorage implements XOStorage {
/**
* Lifecycle state for this in-memory adapter instance.
*/
private databaseState: DatabaseState = DatabaseState.Uninitialized;
/**
* Pending initialization promise used to deduplicate concurrent initialize calls.
*/
private databaseInitializationPromise: Promise<void> | null = null;
/**
* Template records keyed by template identifier.
*/
private templates = new Map<string, XOTemplate>();
/**
* Invitation records keyed by invitation identifier.
*/
private invitations = new Map<string, XOInvitation>();
/**
* Script hash records keyed by script hash.
*/
private scriptHashes = new Map<string, ScriptHashData>();
/**
* Unspent output records keyed by outpoint hash:index.
*/
private unspentOutputs = new Map<string, UnspentOutputData>();
/**
* Event records keyed by event identifier.
*/
private events = new Map<string, EventData>();
/**
* Initializes the storage adapter.
*
* @returns Promise that resolves when initialization is complete
* @throws {DatabaseClosedError} if the adapter has already been closed
*/
async initialize(): Promise<void> {
if (this.databaseState === DatabaseState.Ready) {
return;
}
if (this.databaseState === DatabaseState.Closed) {
throw new DatabaseClosedError();
}
if (this.databaseInitializationPromise !== null) {
return this.databaseInitializationPromise;
}
this.databaseInitializationPromise = Promise.resolve()
.then(() => {
this.databaseState = DatabaseState.Ready;
})
.finally(() => {
this.databaseInitializationPromise = null;
});
return this.databaseInitializationPromise;
}
/**
* Retrieves a template from storage by its identifier.
*
* @param templateIdentifier - Identifier for the template
* @returns Promise resolving to the template data, or undefined if not found
*/
async getTemplate(templateIdentifier: string): Promise<XOTemplate | undefined> {
this.ensureReady();
const template = this.templates.get(templateIdentifier);
return template === undefined ? undefined : this.clone(template);
}
/**
* Stores a template in storage.
*
* @param template - The template data to store
* @returns Promise that resolves when the template is stored
*/
async storeTemplate(template: XOTemplate): Promise<void> {
this.ensureReady();
const templateIdentifier = generateTemplateIdentifier(template);
this.templates.set(templateIdentifier, this.clone(template));
}
/**
* Lists all templates stored in storage.
*
* @returns Promise resolving to all stored templates
*/
async listTemplates(): Promise<XOTemplate[]> {
this.ensureReady();
return Array.from(this.templates.values(), (template) => this.clone(template));
}
/**
* Retrieves an invitation from storage by its identifier.
*
* @param invitationIdentifier - Identifier for the invitation
* @returns Promise resolving to the invitation data, or undefined if not found
*/
async getInvitation(invitationIdentifier: string): Promise<XOInvitation | undefined> {
this.ensureReady();
const invitation = this.invitations.get(invitationIdentifier);
return invitation === undefined ? undefined : this.clone(invitation);
}
/**
* Stores an invitation in storage.
*
* @param invitationIdentifier - Identifier for the invitation
* @param invitation - The invitation data to store
* @returns Promise that resolves when the invitation is stored
*/
async storeInvitation(invitationIdentifier: string, invitation: XOInvitation): Promise<void> {
this.ensureReady();
this.invitations.set(invitationIdentifier, this.clone(invitation));
}
/**
* Retrieves script hash data from storage by script hash.
*
* @param scriptHash - Script hash to retrieve
* @returns Promise resolving to script hash data, or undefined if not found
*/
async getScriptHashData(scriptHash: string): Promise<ScriptHashData | undefined> {
this.ensureReady();
const scriptHashData = this.scriptHashes.get(scriptHash);
return scriptHashData === undefined ? undefined : this.clone(scriptHashData);
}
/**
* Stores script hash data in storage.
*
* @param data - Script hash data to store
* @returns Promise that resolves when data is stored
*/
async storeScriptHashData(data: ScriptHashData): Promise<void> {
this.ensureReady();
this.scriptHashes.set(data.scriptHash, this.clone(data));
}
/**
* Gets the last derivative index used for standard locking bytecodes.
* Filters by templateIdentifier and outputIdentifier, and optionally by invitationIdentifier.
*
* @param templateIdentifier - The template identifier to filter by
* @param outputIdentifier - The output identifier to filter by
* @param invitationIdentifier - Optional invitation identifier to filter by
* @returns Promise resolving to the last derivative index as a string, or null if none found
*/
async getLastDerivativeIndexForP2PKH(
templateIdentifier: string,
outputIdentifier: string,
invitationIdentifier?: string,
): Promise<string | null> {
this.ensureReady();
const allLockingBytecodes = await this.listScriptHashData({ templateIdentifier });
const filtered = allLockingBytecodes.filter((lockingBytecodeData) =>
lockingBytecodeData.derivationSource?.lockingType === 'standard'
&& lockingBytecodeData.outputIdentifier === outputIdentifier
&& lockingBytecodeData.derivationSource?.derivativeIndex !== undefined
&& (invitationIdentifier === undefined || lockingBytecodeData.invitationIdentifier === invitationIdentifier));
if (filtered.length === 0) {
return null;
}
return filtered.reduce((maximumDerivativeIndex, lockingBytecodeData) => {
const currentDerivativeIndex = lockingBytecodeData.derivationSource?.derivativeIndex ?? '0';
const currentNum = parseInt(currentDerivativeIndex, 10);
const maxNum = parseInt(maximumDerivativeIndex, 10);
return currentNum > maxNum ? currentDerivativeIndex : maximumDerivativeIndex;
}, '0');
}
/**
* Retrieves unspent output data from storage.
*
* @param outpointTransactionHash - Outpoint transaction hash
* @param outpointIndex - Outpoint index
* @returns Promise resolving to unspent output data, or undefined if not found
*/
async getUnspentOutputData(outpointTransactionHash: string, outpointIndex: number): Promise<UnspentOutputData | undefined> {
this.ensureReady();
const key = this.getUnspentOutputKey(outpointTransactionHash, outpointIndex);
const output = this.unspentOutputs.get(key);
return output === undefined ? undefined : this.clone(output);
}
/**
* Stores unspent output data in storage.
*
* @param data - Unspent output data to store
* @returns Promise that resolves when data is stored
*/
async storeUnspentOutputData(data: UnspentOutputData): Promise<void> {
this.ensureReady();
const key = this.getUnspentOutputKey(data.outpointTransactionHash, data.outpointIndex);
this.unspentOutputs.set(key, this.clone(data));
}
/**
* Updates the status of an existing unspent output.
*
* @param outpointTransactionHash - Outpoint transaction hash
* @param outpointIndex - Outpoint index
* @param status - New status to set
* @returns Promise that resolves when the update is complete
* @throws {UnspentOutputNotFoundInStoreError} if the output doesn't exist
*/
async updateUnspentOutputDataStatus(outpointTransactionHash: string, outpointIndex: number, status: UnspentOutputStatus): Promise<void> {
this.ensureReady();
const key = this.getUnspentOutputKey(outpointTransactionHash, outpointIndex);
const existing = this.unspentOutputs.get(key);
if (existing === undefined) {
throw new UnspentOutputNotFoundInStoreError(key);
}
this.unspentOutputs.set(key, {
...existing,
status,
});
}
/**
* Sets reservation status of unspent outputs in bulk.
*
* @param outpoints - The outpoints to reserve or unreserve
* @param shouldBeReserved - Whether outputs should be reserved
* @param invitationIdentifier - Invitation identifier owning the reservation
* @returns Promise that resolves when reservation updates complete
* @throws {UnspentOutputNotFoundInStoreError} if any output doesn't exist
* @throws {UnspentOutputReservationConflictError} if any output is held by another invitation
*/
async executeBulkUnspentOutputReservation(
outpoints: Array<{ outpointTransactionHash: string; outpointIndex: number }>,
shouldBeReserved: boolean,
invitationIdentifier: string,
): Promise<void> {
this.ensureReady();
const outputsToUpdate: Array<{ storageKey: string; updatedOutput: UnspentOutputData }> = [];
for (const { outpointTransactionHash, outpointIndex } of outpoints) {
const storageKey = this.getUnspentOutputKey(outpointTransactionHash, outpointIndex);
const existingOutput = this.unspentOutputs.get(storageKey);
if (existingOutput === undefined) {
throw new UnspentOutputNotFoundInStoreError(storageKey);
}
if (shouldBeReserved) {
if (existingOutput.reservedBy === invitationIdentifier) {
continue;
}
if (existingOutput.reservedBy !== undefined) {
throw new UnspentOutputReservationConflictError(storageKey, existingOutput.reservedBy);
}
} else {
if (existingOutput.reservedBy === undefined) {
continue;
}
if (existingOutput.reservedBy !== invitationIdentifier) {
throw new UnspentOutputReservationConflictError(storageKey, existingOutput.reservedBy);
}
}
outputsToUpdate.push({
storageKey,
updatedOutput: {
...existingOutput,
reservedBy: shouldBeReserved ? invitationIdentifier : undefined,
},
});
}
for (const { storageKey, updatedOutput } of outputsToUpdate) {
this.unspentOutputs.set(storageKey, updatedOutput);
}
}
/**
* Deletes unspent output data from storage.
*
* @param outpointTransactionHash - Outpoint transaction hash
* @param outpointIndex - Outpoint index
* @returns Promise that resolves when deletion is complete
*/
async deleteUnspentOutputData(outpointTransactionHash: string, outpointIndex: number): Promise<void> {
this.ensureReady();
const key = this.getUnspentOutputKey(outpointTransactionHash, outpointIndex);
this.unspentOutputs.delete(key);
}
/**
* Lists script hash data matching the provided query.
*
* @param query - Query to apply
* @returns Promise resolving to matching script hash records
* @throws {QueryFieldsLimitExceededError} if more than one query field is provided
* @throws {QueryInvalidError} if the query contains no recognized field
*/
async listScriptHashData(query: ScriptHashDataQuery): Promise<ScriptHashData[]> {
this.ensureReady();
const definedFields = getDefinedQueryFields(query);
if (definedFields.length > 1) {
throw new QueryFieldsLimitExceededError('scriptHashes', definedFields);
}
if (query.templateIdentifier !== undefined) {
return this.filterScriptHashes((item) => item.templateIdentifier === query.templateIdentifier);
}
if (query.invitationIdentifier !== undefined) {
return this.filterScriptHashes((item) => item.invitationIdentifier === query.invitationIdentifier);
}
if (query.scriptHash !== undefined) {
return this.filterScriptHashes((item) => item.scriptHash === query.scriptHash);
}
throw new QueryInvalidError('scriptHashes');
}
/**
* Lists unspent output data matching query and optional in-memory filter.
*
* @param query - Query for selecting unspent outputs
* @param filter - Optional filter for reserved status
* @returns Promise resolving to matching unspent outputs
* @throws {QueryFieldsLimitExceededError} if more than one query field is provided
*/
async listUnspentOutputs(query: UnspentOutputDataQuery = {}, filter?: UnspentOutputDataFilter): Promise<UnspentOutputData[]> {
this.ensureReady();
const definedFields = getDefinedQueryFields(query);
if (definedFields.length > 1) {
throw new QueryFieldsLimitExceededError('unspentOutputs', definedFields);
}
let unspentOutputs = Array.from(this.unspentOutputs.values(), (output) => this.clone(output));
if (query.templateIdentifier !== undefined) {
unspentOutputs = unspentOutputs.filter((output) => output.templateIdentifier === query.templateIdentifier);
} else if (query.reservedBy !== undefined) {
unspentOutputs = unspentOutputs.filter((output) => output.reservedBy === query.reservedBy);
} else if (query.scriptHash !== undefined) {
unspentOutputs = unspentOutputs.filter((output) => output.scriptHash === query.scriptHash);
} else if (query.status !== undefined) {
unspentOutputs = unspentOutputs.filter((output) => output.status === query.status);
}
if (filter?.reserved !== undefined) {
unspentOutputs = unspentOutputs.filter((unspentOutput) => (unspentOutput.reservedBy !== undefined) === filter.reserved);
}
return unspentOutputs;
}
/**
* Stores an event in storage.
*
* @param eventId - Event identifier
* @param eventType - Event type
* @param data - Event payload
* @param timestamp - Optional timestamp, defaults to Date.now()
* @returns Promise that resolves when event is stored
*/
async storeEvent(eventId: string, eventType: string, data: unknown, timestamp?: number): Promise<void> {
this.ensureReady();
this.events.set(eventId, {
eventId,
timestamp: timestamp ?? Date.now(),
eventType,
data: this.clone(data),
});
}
/**
* Lists event data matching the provided query.
*
* @param query - Query to apply
* @returns Promise resolving to matching events
* @throws {QueryFieldsLimitExceededError} if more than one query field is provided
* @throws {QueryInvalidError} if the query contains no recognized field
*/
async listEvents(query: EventDataQuery): Promise<EventData[]> {
this.ensureReady();
const definedFields = getDefinedQueryFields(query);
if (definedFields.length > 1) {
throw new QueryFieldsLimitExceededError('events', definedFields);
}
if (query.eventType !== undefined) {
return Array.from(this.events.values())
.filter((event) => event.eventType === query.eventType)
.map((event) => this.clone(event));
}
throw new QueryInvalidError('events');
}
/**
* Closes the storage adapter and releases in-memory resources.
*
* @returns Promise that resolves when close operation finishes
*/
async close(): Promise<void> {
this.templates.clear();
this.invitations.clear();
this.scriptHashes.clear();
this.unspentOutputs.clear();
this.events.clear();
this.databaseState = DatabaseState.Closed;
this.databaseInitializationPromise = null;
}
/**
* Ensures the adapter is in a ready state before handling any operation.
* Mirrors the behavior of the IndexedDB implementation where calls after its been closed throw an error.
*
* @throws {DatabaseClosedError} if the adapter has already been closed
* @throws {DatabaseNotInitializedError} if initialize has not been called
*/
private ensureReady(): void {
if (this.databaseState === DatabaseState.Closed) {
throw new DatabaseClosedError();
}
}
/**
* Builds the canonical storage key for unspent outputs.
*
* @param outpointTransactionHash - Outpoint transaction hash
* @param outpointIndex - Outpoint index
* @returns Canonical key in hash:index format
*/
private getUnspentOutputKey(outpointTransactionHash: string, outpointIndex: number): string {
return `${outpointTransactionHash}:${outpointIndex}`;
}
/**
* Returns script hash records matching the given predicate.
*
* @param predicate - Predicate used to filter script hash records
* @returns Matching script hash records
*/
private filterScriptHashes(predicate: (item: ScriptHashData) => boolean): ScriptHashData[] {
return Array.from(this.scriptHashes.values())
.filter(predicate)
.map((item) => this.clone(item));
}
/**
* Creates a detached clone to preserve storage value semantics.
*
* IndexedDB deserializes values when reading, so callers cannot mutate data that is
* still stored in the database. We clone in-memory values to keep that same behavior.
*
* @param value - Value to clone
* @returns Detached clone of the provided value
*/
private clone<T>(value: T): T {
return structuredClone(value);
}
}