Compare commits
5 Commits
def261b568
...
simplified
| Author | SHA1 | Date | |
|---|---|---|---|
|
27022a9390
|
|||
|
33a465eca0
|
|||
|
2442b17a4c
|
|||
|
a79213436c
|
|||
|
d45f1ddeb3
|
@@ -1,904 +0,0 @@
|
|||||||
{
|
|
||||||
"$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": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
4322
package-lock.json
generated
4322
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "SYNC_SERVER_URL=https://sync.xo.harvmaster.com tsx src/index.ts",
|
"dev": "SYNC_SERVER_URL=https://sync.xo.harvmaster.com tsx src/index.ts",
|
||||||
"build": "tsc && npm run build:copy-scripts",
|
"build": "tsc --nocheck --noEmitOnError false || true && npm run build:copy-scripts",
|
||||||
|
"build:safe": "tsc && npm run build:copy-scripts",
|
||||||
"build:copy-scripts": "cp -r src/cli/autocomplete/scripts dist/cli/autocomplete/",
|
"build:copy-scripts": "cp -r src/cli/autocomplete/scripts dist/cli/autocomplete/",
|
||||||
"start": "SYNC_SERVER_URL=https://sync.xo.harvmaster.com node dist/index.js",
|
"start": "SYNC_SERVER_URL=https://sync.xo.harvmaster.com node dist/index.js",
|
||||||
"test": "vitest --run --passWithNoTests",
|
"test": "vitest --run --passWithNoTests",
|
||||||
@@ -38,10 +39,9 @@
|
|||||||
"@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": "file:../state",
|
"@xo-cash/state": "0.0.1",
|
||||||
"@xo-cash/templates": "file:../templates",
|
"@xo-cash/templates": "^0.0.3",
|
||||||
"@xo-cash/types": "^0.0.1",
|
"@xo-cash/types": "^0.0.1",
|
||||||
"better-sqlite3": "^12.6.2",
|
|
||||||
"clipboardy": "^5.1.0",
|
"clipboardy": "^5.1.0",
|
||||||
"ink": "^6.6.0",
|
"ink": "^6.6.0",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
@@ -50,7 +50,6 @@
|
|||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
|
||||||
"@types/node": "^25.0.10",
|
"@types/node": "^25.0.10",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
|
|||||||
36
readme.md
36
readme.md
@@ -27,42 +27,6 @@ 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 https://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 Template Setup ----
|
|
||||||
# Clone the Template repo
|
|
||||||
git clone https://gitlab.com/Harvmaster/templates.git
|
|
||||||
|
|
||||||
# Move into themplates directory
|
|
||||||
cd templates
|
|
||||||
|
|
||||||
# Install deps
|
|
||||||
npm ci
|
|
||||||
|
|
||||||
#build the templates
|
|
||||||
npm run build
|
|
||||||
# ----- End Templates 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 https://git.harvmaster.com/Harvmaster/xo-cli.git
|
||||||
|
|||||||
@@ -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 page pencil stock planet limb cluster assault speak off joke private pioneer
|
xo-cli mnemonic import oven crop same above under tower promote decrease vocal pretty require slow
|
||||||
|
|
||||||
# List mnemonic basenames (use with -m)
|
# List mnemonic basenames (use with -m)
|
||||||
xo-cli mnemonic list
|
xo-cli mnemonic list
|
||||||
|
|||||||
@@ -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 page pencil stock planet limb cluster assault speak off joke private pioneer -v -o mnemonic.txt` will return:
|
* eg: `xo-cli mnemonic create oven crop same above under tower promote decrease vocal pretty require slow -v -o mnemonic.txt` will return:
|
||||||
* {
|
* {
|
||||||
* args: ["mnemonic", "create", "page", "pencil", "stock", "planet", "limb", "cluster", "assault", "speak", "off", "joke", "private", "pioneer"],
|
* args: ["mnemonic", "create", "oven", "crop", "same", "above", "under", "tower", "promote", "decrease", "vocal", "pretty", "require", "slow"],
|
||||||
* options: {
|
* options: {
|
||||||
* output: "mnemonic.txt",
|
* output: "mnemonic.txt",
|
||||||
* verbose: "true",
|
* verbose: "true",
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ import { homedir } from "node:os";
|
|||||||
*
|
*
|
||||||
* IMPORTANT: Keep this in sync with actual switch statements in command handlers:
|
* IMPORTANT: Keep this in sync with actual switch statements in command handlers:
|
||||||
* - mnemonic.ts: create, import, list, expose
|
* - mnemonic.ts: create, import, list, expose
|
||||||
* - template.ts: import, list, inspect, export, set-default
|
* - template.ts: import, list, inspect, set-default
|
||||||
* - invitation.ts: create, append, sign, broadcast, requirements, import, export, inspect, list
|
* - invitation.ts: create, append, sign, broadcast, requirements, import, inspect, list
|
||||||
* - resource.ts: list, unreserve, unreserve-all
|
* - resource.ts: list, unreserve, unreserve-all
|
||||||
* - settings.ts: show, get, set
|
* - settings.ts: show, get, set
|
||||||
*/
|
*/
|
||||||
@@ -43,7 +43,7 @@ import { homedir } from "node:os";
|
|||||||
/** Subcommands for the mnemonic command */
|
/** Subcommands for the mnemonic command */
|
||||||
const MNEMONIC_SUBS = ["create", "import", "list", "expose"];
|
const MNEMONIC_SUBS = ["create", "import", "list", "expose"];
|
||||||
/** Subcommands for the template command */
|
/** Subcommands for the template command */
|
||||||
const TEMPLATE_SUBS = ["import", "list", "inspect", "export", "set-default"];
|
const TEMPLATE_SUBS = ["import", "list", "inspect", "set-default"];
|
||||||
/** Subcommands for the invitation command */
|
/** Subcommands for the invitation command */
|
||||||
const INVITATION_SUBS = [
|
const INVITATION_SUBS = [
|
||||||
"create",
|
"create",
|
||||||
@@ -52,7 +52,6 @@ const INVITATION_SUBS = [
|
|||||||
"broadcast",
|
"broadcast",
|
||||||
"requirements",
|
"requirements",
|
||||||
"import",
|
"import",
|
||||||
"export",
|
|
||||||
"inspect",
|
"inspect",
|
||||||
"list",
|
"list",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,7 +8,11 @@
|
|||||||
* and instead constructs the engine directly with an in-memory blockchain provider.
|
* and instead constructs the engine directly with an in-memory blockchain provider.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BlockchainMonitor, Engine } from "@xo-cash/engine";
|
import {
|
||||||
|
BlockchainMonitor,
|
||||||
|
Engine,
|
||||||
|
InMemoryBlockchainProvider,
|
||||||
|
} from "@xo-cash/engine";
|
||||||
import { createStorageAdapter, State, StorageType } from "@xo-cash/state";
|
import { createStorageAdapter, State, StorageType } from "@xo-cash/state";
|
||||||
import { convertMnemonicToSeedBytes } from "@xo-cash/crypto";
|
import { convertMnemonicToSeedBytes } from "@xo-cash/crypto";
|
||||||
import { binToHex, hash256 } from "@bitauth/libauth";
|
import { binToHex, hash256 } from "@bitauth/libauth";
|
||||||
@@ -63,21 +67,18 @@ export async function createOfflineEngine(
|
|||||||
// Create the state instance
|
// Create the state instance
|
||||||
const state = new State(storageAdapter);
|
const state = new State(storageAdapter);
|
||||||
|
|
||||||
// Create a minimal blockchain monitor (no electrum initialization)
|
// Use in-memory blockchain provider (no network connections)
|
||||||
const blockchainMonitor = new BlockchainMonitor(state);
|
const blockchainProvider = new InMemoryBlockchainProvider();
|
||||||
|
await blockchainProvider.initialize({
|
||||||
|
applicationIdentifier: "xo-cli-completions",
|
||||||
|
electrumOptions: {},
|
||||||
|
});
|
||||||
|
|
||||||
// Engine constructor is private; bypass for offline read-only completions.
|
// Create a minimal blockchain monitor
|
||||||
type EngineConstructor = new (
|
const blockchainMonitor = new BlockchainMonitor(state, blockchainProvider);
|
||||||
mnemonic: string,
|
|
||||||
state: State,
|
|
||||||
blockchainMonitor: BlockchainMonitor,
|
|
||||||
) => Engine;
|
|
||||||
|
|
||||||
const engine = new (Engine as unknown as EngineConstructor)(
|
// Construct engine directly without state sync
|
||||||
seed,
|
const engine = new Engine(seed, state, blockchainMonitor, blockchainProvider);
|
||||||
state,
|
|
||||||
blockchainMonitor,
|
|
||||||
);
|
|
||||||
|
|
||||||
return engine;
|
return engine;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ _{{FUNC_NAME}}_completions() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
append|sign|broadcast|requirements|export|inspect)
|
append|sign|broadcast|requirements|inspect)
|
||||||
# These subcommands expect an invitation identifier as first arg.
|
# These subcommands expect an invitation identifier as first arg.
|
||||||
local pos=$((cword - subcmd_idx))
|
local pos=$((cword - subcmd_idx))
|
||||||
if [[ $pos -eq 1 ]]; then
|
if [[ $pos -eq 1 ]]; then
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_
|
|||||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from sign; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic invitations)'
|
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from sign; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic invitations)'
|
||||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from broadcast; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic invitations)'
|
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from broadcast; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic invitations)'
|
||||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from requirements; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic invitations)'
|
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from requirements; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic invitations)'
|
||||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from export; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic invitations)'
|
|
||||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from inspect; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic invitations)'
|
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from inspect; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic invitations)'
|
||||||
|
|
||||||
# invitation import <path>
|
# invitation import <path>
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ _{{FUNC_NAME}}_completions() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
append|sign|broadcast|requirements|export|inspect)
|
append|sign|broadcast|requirements|inspect)
|
||||||
# These subcommands take invitation ID as first argument.
|
# These subcommands take invitation ID as first argument.
|
||||||
local pos=$((CURRENT - subcmd_idx))
|
local pos=$((CURRENT - subcmd_idx))
|
||||||
if [[ $pos -eq 1 ]]; then
|
if [[ $pos -eq 1 ]]; then
|
||||||
|
|||||||
@@ -11,21 +11,13 @@ import {
|
|||||||
resolveProvidedLockingBytecodeHex,
|
resolveProvidedLockingBytecodeHex,
|
||||||
mapUnspentOutputsToSelectable,
|
mapUnspentOutputsToSelectable,
|
||||||
autoSelectGreedyUtxos,
|
autoSelectGreedyUtxos,
|
||||||
hasMissingRequirements,
|
|
||||||
} from "../../utils/invitation-flow.js";
|
} from "../../utils/invitation-flow.js";
|
||||||
import { deserializeInvitation, serializeInvitation } from "@xo-cash/engine";
|
import { encodeExtendedJson } from "../../utils/ext-json.js";
|
||||||
import type { XOInvitation } from "@xo-cash/types";
|
|
||||||
import { resolveTemplate } from "../utils.js";
|
import { resolveTemplate } from "../utils.js";
|
||||||
|
|
||||||
const DEFAULT_FEE = 500n;
|
const DEFAULT_FEE = 500n;
|
||||||
const DUST_THRESHOLD = 546n;
|
const DUST_THRESHOLD = 546n;
|
||||||
|
|
||||||
/**
|
|
||||||
* Serializes an invitation to pretty-printed JSON for file export.
|
|
||||||
*/
|
|
||||||
const formatInvitationForFile = (invitation: XOInvitation, indent = 2): string =>
|
|
||||||
JSON.stringify(JSON.parse(serializeInvitation(invitation)), null, indent);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result of parsing CLI options into inputs and outputs for an append call.
|
* Result of parsing CLI options into inputs and outputs for an append call.
|
||||||
* A `null` return signals a fatal error that was already logged to stderr.
|
* A `null` return signals a fatal error that was already logged to stderr.
|
||||||
@@ -294,13 +286,9 @@ ${bold("Sub-commands:")}
|
|||||||
- broadcast <invitation-id> ${dim("Broadcast an invitation")}
|
- broadcast <invitation-id> ${dim("Broadcast an invitation")}
|
||||||
- requirements <invitation-id> ${dim("Show requirements for an invitation")}
|
- requirements <invitation-id> ${dim("Show requirements for an invitation")}
|
||||||
- import <invitation-file> ${dim("Import an invitation from a file")}
|
- import <invitation-file> ${dim("Import an invitation from a file")}
|
||||||
- export <invitation-id> [output-file] ${dim("Export an invitation to stdout or a file")}
|
|
||||||
- inspect <invitation-id | invitation-file> ${dim("Inspect an invitation")}
|
- inspect <invitation-id | invitation-file> ${dim("Inspect an invitation")}
|
||||||
- list ${dim("List all invitations")}
|
- list ${dim("List all invitations")}
|
||||||
|
|
||||||
${bold("Export options:")}
|
|
||||||
-o --output <output-filename> ${dim("Output filename for the exported invitation")}
|
|
||||||
|
|
||||||
${bold("Create / Append options:")}
|
${bold("Create / Append options:")}
|
||||||
-var-<name> <value> ${dim("Set a variable (e.g. -var-requested-satoshis 1000)")}
|
-var-<name> <value> ${dim("Set a variable (e.g. -var-requested-satoshis 1000)")}
|
||||||
--add-input <txhash:vout> ${dim("Add UTXO input(s), comma-separated (e.g. abc123:0,def456:1)")}
|
--add-input <txhash:vout> ${dim("Add UTXO input(s), comma-separated (e.g. abc123:0,def456:1)")}
|
||||||
@@ -323,7 +311,6 @@ export type InvitationCommandResult = {
|
|||||||
invitationIdentifier?: string;
|
invitationIdentifier?: string;
|
||||||
txHash?: string;
|
txHash?: string;
|
||||||
count?: number;
|
count?: number;
|
||||||
outputFile?: string;
|
|
||||||
templateName?: string;
|
templateName?: string;
|
||||||
actionIdentifier?: string;
|
actionIdentifier?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
@@ -333,66 +320,6 @@ export type InvitationCommandResult = {
|
|||||||
variables?: unknown[];
|
variables?: unknown[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the invitation export command.
|
|
||||||
* Throws CommandError on failure, returns result data on success.
|
|
||||||
* @param deps - The command dependencies.
|
|
||||||
* @param args - Positional args after "export", e.g. ["invitation-id"] or ["invitation-id", "invitation.json"].
|
|
||||||
* @param options - Parsed option flags.
|
|
||||||
*/
|
|
||||||
export const handleInvitationExportCommand = async (
|
|
||||||
deps: CommandDependencies,
|
|
||||||
args: string[],
|
|
||||||
options: Record<string, string>,
|
|
||||||
): Promise<{ invitationIdentifier?: string; outputFile?: string }> => {
|
|
||||||
const invitationIdentifier = args[0];
|
|
||||||
deps.io.verbose(`Invitation identifier: ${invitationIdentifier}`);
|
|
||||||
|
|
||||||
if (!invitationIdentifier) {
|
|
||||||
deps.io.verbose("No invitation identifier provided");
|
|
||||||
printInvitationHelp(deps.io);
|
|
||||||
throw new CommandError(
|
|
||||||
"invitation.export.identifier_missing",
|
|
||||||
"No invitation identifier provided",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const invitation = deps.app.invitations.find(
|
|
||||||
(candidate) =>
|
|
||||||
candidate.data.invitationIdentifier === invitationIdentifier,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!invitation) {
|
|
||||||
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
|
|
||||||
throw new CommandError(
|
|
||||||
"invitation.export.not_found",
|
|
||||||
`Invitation not found: ${invitationIdentifier}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const serializedInvitation = serializeInvitation(invitation.data);
|
|
||||||
|
|
||||||
const outputFile = options["output"] ?? args[1];
|
|
||||||
|
|
||||||
if (!outputFile) {
|
|
||||||
deps.io.out(serializedInvitation);
|
|
||||||
return { invitationIdentifier };
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputPath = path.resolve(process.cwd(), outputFile);
|
|
||||||
try {
|
|
||||||
writeFileSync(outputPath, serializedInvitation);
|
|
||||||
} catch (error) {
|
|
||||||
throw new CommandError(
|
|
||||||
"invitation.export.write_failed",
|
|
||||||
`Failed to export invitation to file: ${outputPath} (${error instanceof Error ? error.message : "unknown error"})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
deps.io.out(`Invitation exported to: ${outputPath}`);
|
|
||||||
return { invitationIdentifier, outputFile: outputPath };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the invitation command.
|
* Handles the invitation command.
|
||||||
* Throws CommandError on failure, returns result data on success.
|
* Throws CommandError on failure, returns result data on success.
|
||||||
@@ -484,7 +411,7 @@ export const handleInvitationCommand = async (
|
|||||||
deps.io.verbose(`Invitation file path: ${invitationFilePath}`);
|
deps.io.verbose(`Invitation file path: ${invitationFilePath}`);
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
invitationFilePath,
|
invitationFilePath,
|
||||||
formatInvitationForFile(invitationInstance.data),
|
encodeExtendedJson(invitationInstance.data, 2),
|
||||||
);
|
);
|
||||||
deps.io.out(
|
deps.io.out(
|
||||||
`Invitation created: ${path.basename(invitationFilePath)} (${invitationInstance.data.invitationIdentifier})`,
|
`Invitation created: ${path.basename(invitationFilePath)} (${invitationInstance.data.invitationIdentifier})`,
|
||||||
@@ -494,8 +421,11 @@ export const handleInvitationCommand = async (
|
|||||||
const missingRequirements =
|
const missingRequirements =
|
||||||
await invitationInstance.getMissingRequirements();
|
await invitationInstance.getMissingRequirements();
|
||||||
const hasMissing =
|
const hasMissing =
|
||||||
hasMissingRequirements(missingRequirements.templateRequirements) ||
|
(missingRequirements.variables?.length ?? 0) > 0 ||
|
||||||
missingRequirements.inputsMissingSignatures.length > 0;
|
(missingRequirements.inputs?.length ?? 0) > 0 ||
|
||||||
|
(missingRequirements.outputs?.length ?? 0) > 0 ||
|
||||||
|
(missingRequirements.roles !== undefined &&
|
||||||
|
Object.keys(missingRequirements.roles).length > 0);
|
||||||
|
|
||||||
// If there are missing requirements, print them out
|
// If there are missing requirements, print them out
|
||||||
if (hasMissing) {
|
if (hasMissing) {
|
||||||
@@ -602,10 +532,7 @@ export const handleInvitationCommand = async (
|
|||||||
// Write the invitation to a file in the working directory
|
// Write the invitation to a file in the working directory
|
||||||
// TODO: Support the -o flag to specify the output path
|
// TODO: Support the -o flag to specify the output path
|
||||||
const invitationFilePath = `${deps.paths.workingDir}/inv-${invitation.data.invitationIdentifier}.json`;
|
const invitationFilePath = `${deps.paths.workingDir}/inv-${invitation.data.invitationIdentifier}.json`;
|
||||||
writeFileSync(
|
writeFileSync(invitationFilePath, encodeExtendedJson(invitation.data, 2));
|
||||||
invitationFilePath,
|
|
||||||
formatInvitationForFile(invitation.data),
|
|
||||||
);
|
|
||||||
deps.io.out(
|
deps.io.out(
|
||||||
`Invitation updated: ${path.basename(invitationFilePath)} (${invitation.data.invitationIdentifier})`,
|
`Invitation updated: ${path.basename(invitationFilePath)} (${invitation.data.invitationIdentifier})`,
|
||||||
);
|
);
|
||||||
@@ -613,8 +540,11 @@ export const handleInvitationCommand = async (
|
|||||||
// Get the missing requirements for the invitation. This will tell us if we are missing any variables, inputs, outputs, or roles.
|
// Get the missing requirements for the invitation. This will tell us if we are missing any variables, inputs, outputs, or roles.
|
||||||
const missingRequirements = await invitation.getMissingRequirements();
|
const missingRequirements = await invitation.getMissingRequirements();
|
||||||
const hasMissing =
|
const hasMissing =
|
||||||
hasMissingRequirements(missingRequirements.templateRequirements) ||
|
(missingRequirements.variables?.length ?? 0) > 0 ||
|
||||||
missingRequirements.inputsMissingSignatures.length > 0;
|
(missingRequirements.inputs?.length ?? 0) > 0 ||
|
||||||
|
(missingRequirements.outputs?.length ?? 0) > 0 ||
|
||||||
|
(missingRequirements.roles !== undefined &&
|
||||||
|
Object.keys(missingRequirements.roles).length > 0);
|
||||||
|
|
||||||
// If there are missing requirements, print them out
|
// If there are missing requirements, print them out
|
||||||
if (hasMissing) {
|
if (hasMissing) {
|
||||||
@@ -791,10 +721,11 @@ export const handleInvitationCommand = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read the invitation file (XOInvitation format, can be passed to the engine directly)
|
// Read the invitation file (XOInvitation format, can be passed to the engine directly)
|
||||||
const invitationFile = readFileSync(invitationFilePath, "utf8");
|
const invitationFile = await readFileSync(invitationFilePath, "utf8");
|
||||||
deps.io.verbose(`Invitation file: ${invitationFile}`);
|
deps.io.verbose(`Invitation file: ${invitationFile}`);
|
||||||
|
|
||||||
const invitation = deserializeInvitation(invitationFile);
|
// Parse the invitation file
|
||||||
|
const invitation = JSON.parse(invitationFile);
|
||||||
deps.io.verbose(`Invitation: ${formatObject(invitation)}`);
|
deps.io.verbose(`Invitation: ${formatObject(invitation)}`);
|
||||||
|
|
||||||
// Create the invitation instance (NOTE: Not an engine compatible invitation. This is the wrapped format)
|
// Create the invitation instance (NOTE: Not an engine compatible invitation. This is the wrapped format)
|
||||||
@@ -908,27 +839,19 @@ export const handleInvitationCommand = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read the invitation file (XOInvitation format, can be passed to the engine directly)
|
// Read the invitation file (XOInvitation format, can be passed to the engine directly)
|
||||||
const invitationFile = readFileSync(invitationFilePath, "utf8");
|
const invitationFile = await readFileSync(invitationFilePath, "utf8");
|
||||||
deps.io.verbose(`Invitation file: ${invitationFile}`);
|
deps.io.verbose(`Invitation file: ${invitationFile}`);
|
||||||
|
|
||||||
const invitation = deserializeInvitation(invitationFile);
|
// Parse the invitation file
|
||||||
|
const invitation = JSON.parse(invitationFile);
|
||||||
deps.io.verbose(`Invitation: ${formatObject(invitation)}`);
|
deps.io.verbose(`Invitation: ${formatObject(invitation)}`);
|
||||||
|
|
||||||
const template = await deps.app.engine.getTemplate(
|
// "Creates" the invitiation in the engine. This method acts as both creation or import depending on the data that is being passed in
|
||||||
invitation.templateIdentifier,
|
const xoInvitation = await deps.app.engine.createInvitation(invitation);
|
||||||
);
|
deps.io.verbose(`XOInvitation: ${formatObject(xoInvitation)}`);
|
||||||
if (!template) {
|
|
||||||
throw new CommandError(
|
|
||||||
"invitation.import.template_not_found",
|
|
||||||
`Template not found: ${invitation.templateIdentifier}. ` +
|
|
||||||
`Import the matching template first with: xo-cli template import <template-file>`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept and track the invitation. Invitation.create calls acceptInvitation
|
// Create the invitation instance (NOTE: Not an engine compatible invitation. This is the wrapped format)
|
||||||
// internally — do not call engine.createInvitation here, that creates a new
|
const invitationInstance = await deps.app.createInvitation(xoInvitation);
|
||||||
// invitation and discards the imported commits.
|
|
||||||
const invitationInstance = await deps.app.createInvitation(invitation);
|
|
||||||
deps.io.verbose(
|
deps.io.verbose(
|
||||||
`Invitation created: ${formatObject(invitationInstance.data)}`,
|
`Invitation created: ${formatObject(invitationInstance.data)}`,
|
||||||
);
|
);
|
||||||
@@ -939,10 +862,6 @@ export const handleInvitationCommand = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case "export": {
|
|
||||||
return handleInvitationExportCommand(deps, args.slice(1), options);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "list": {
|
case "list": {
|
||||||
// List all the invitations
|
// List all the invitations
|
||||||
const invitations = await Promise.all(
|
const invitations = await Promise.all(
|
||||||
|
|||||||
@@ -2,14 +2,8 @@ import { hexToBin } from "@bitauth/libauth";
|
|||||||
|
|
||||||
import { bold, dim } from "../utils.js";
|
import { bold, dim } from "../utils.js";
|
||||||
import type { CommandDependencies, CommandIO } from "./types.js";
|
import type { CommandDependencies, CommandIO } from "./types.js";
|
||||||
|
import type { UnspentOutputData } from "@xo-cash/state";
|
||||||
import { CommandError } from "./types.js";
|
import { CommandError } from "./types.js";
|
||||||
import type { XOTemplate } from "@xo-cash/types";
|
|
||||||
import { generateTemplateIdentifier } from "@xo-cash/engine";
|
|
||||||
import {
|
|
||||||
buildScriptHashDataMap,
|
|
||||||
enrichUnspentOutput,
|
|
||||||
type UnspentOutputWithMetadata,
|
|
||||||
} from "../../utils/utxo-metadata.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prints the help message for the resource command.
|
* Prints the help message for the resource command.
|
||||||
@@ -33,12 +27,9 @@ ${bold("Sub-commands:")}
|
|||||||
* Formats a single UTXO for display, optionally including reservation info.
|
* Formats a single UTXO for display, optionally including reservation info.
|
||||||
*/
|
*/
|
||||||
function formatResource(
|
function formatResource(
|
||||||
resource: UnspentOutputWithMetadata & { template?: XOTemplate },
|
resource: UnspentOutputData,
|
||||||
showReserved = false,
|
showReserved = false,
|
||||||
): string {
|
): string {
|
||||||
// Format the template
|
|
||||||
const template = resource.template ? dim(`[${generateTemplateIdentifier(resource.template)}]`) : "";
|
|
||||||
|
|
||||||
// Format the outpoint
|
// Format the outpoint
|
||||||
const outpoint = bold(
|
const outpoint = bold(
|
||||||
`${resource.outpointTransactionHash}:${resource.outpointIndex}`,
|
`${resource.outpointTransactionHash}:${resource.outpointIndex}`,
|
||||||
@@ -48,9 +39,7 @@ function formatResource(
|
|||||||
const value = dim(`${resource.valueSatoshis} sats`);
|
const value = dim(`${resource.valueSatoshis} sats`);
|
||||||
|
|
||||||
// Format the output
|
// Format the output
|
||||||
const output = resource.outputIdentifier
|
const output = dim(resource.outputIdentifier);
|
||||||
? dim(resource.outputIdentifier)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
// Format the height
|
// Format the height
|
||||||
const height = dim(`(height ${resource.minedAtHeight})`);
|
const height = dim(`(height ${resource.minedAtHeight})`);
|
||||||
@@ -58,11 +47,11 @@ function formatResource(
|
|||||||
// If the resource is reserved, format the reservation info
|
// If the resource is reserved, format the reservation info
|
||||||
if (showReserved && resource.reservedBy) {
|
if (showReserved && resource.reservedBy) {
|
||||||
const inv = dim(`reserved for ${resource.reservedBy}`);
|
const inv = dim(`reserved for ${resource.reservedBy}`);
|
||||||
return `${template} ${outpoint} ${value} ${output} ${height} ${inv}`;
|
return `${outpoint} ${value} ${output} ${height} ${inv}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, format the resource without reservation info
|
// Otherwise, format the resource without reservation info
|
||||||
return `${template} ${outpoint} ${value} ${output} ${height}`;
|
return `${outpoint} ${value} ${output} ${height}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,30 +108,9 @@ export const handleResourceCommand = async (
|
|||||||
return { count: 0 };
|
return { count: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const scriptHashDataByScriptHash = await buildScriptHashDataMap(
|
|
||||||
deps.app.engine,
|
|
||||||
);
|
|
||||||
|
|
||||||
const resourcesWithTemplateInformation = await Promise.all(
|
|
||||||
filtered.map(async (resource) => {
|
|
||||||
const enriched = enrichUnspentOutput(
|
|
||||||
resource,
|
|
||||||
scriptHashDataByScriptHash,
|
|
||||||
);
|
|
||||||
const template = enriched.templateIdentifier
|
|
||||||
? await deps.app.engine.getTemplate(enriched.templateIdentifier)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...enriched,
|
|
||||||
template,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Format the resources into a list of strings that we can display to the user
|
// Format the resources into a list of strings that we can display to the user
|
||||||
const showReserved = qualifier === "all" || qualifier === "reserved";
|
const showReserved = qualifier === "all" || qualifier === "reserved";
|
||||||
const formattedResources = resourcesWithTemplateInformation.map((r) =>
|
const formattedResources = filtered.map((r) =>
|
||||||
formatResource(r, showReserved),
|
formatResource(r, showReserved),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
import { existsSync, readFileSync } from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { generateTemplateIdentifier } from "@xo-cash/engine";
|
import { generateTemplateIdentifier } from "@xo-cash/engine";
|
||||||
import type { XOTemplate } from "@xo-cash/types";
|
import type { XOTemplate } from "@xo-cash/types";
|
||||||
@@ -23,10 +23,6 @@ ${bold("Sub-commands:")}
|
|||||||
- list <category> <identifier> ${dim("List all options of the field type in a template")}
|
- list <category> <identifier> ${dim("List all options of the field type in a template")}
|
||||||
- inspect <category> <identifier> <field> ${dim("Inspect a field in a template")}
|
- inspect <category> <identifier> <field> ${dim("Inspect a field in a template")}
|
||||||
- set-default <template-file> <output-identifier> <role-identifier> ${dim("Set the default template")}
|
- set-default <template-file> <output-identifier> <role-identifier> ${dim("Set the default template")}
|
||||||
- export <template-identifier> [output-file] ${dim("Export a template to stdout or a file")}
|
|
||||||
|
|
||||||
${bold("Options:")}
|
|
||||||
-o --output <output-filename> ${dim("Output filename for the exported template")}
|
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -342,71 +338,6 @@ export const handleTemplateInspectCommand = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the template export command.
|
|
||||||
* Throws CommandError on failure, returns result data on success.
|
|
||||||
* @param deps - The command dependencies.
|
|
||||||
* @param args - Positional args after "export", e.g. ["template-id"] or ["template-id", "template.json"].
|
|
||||||
* @param options - Parsed option flags.
|
|
||||||
*/
|
|
||||||
export const handleTemplateExportCommand = async (
|
|
||||||
deps: CommandDependencies,
|
|
||||||
args: string[],
|
|
||||||
options: Record<string, string>,
|
|
||||||
): Promise<{ outputFile?: string }> => {
|
|
||||||
// Get the template identifier from the arguments
|
|
||||||
const templateIdentifier = args[0];
|
|
||||||
|
|
||||||
// If no template identifier is provided, print a message and throw an error
|
|
||||||
if (!templateIdentifier) {
|
|
||||||
deps.io.err("No template identifier provided");
|
|
||||||
printTemplateHelp(deps.io);
|
|
||||||
throw new CommandError(
|
|
||||||
"template.export.identifier_missing",
|
|
||||||
"No template identifier provided",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the raw template from the engine.
|
|
||||||
// Do not resolve references or pretty-print the template.
|
|
||||||
const rawTemplate = await deps.app.engine.getTemplate(templateIdentifier);
|
|
||||||
|
|
||||||
// If the raw template is not found, print a message and throw an error
|
|
||||||
if (!rawTemplate) {
|
|
||||||
deps.io.err(`No template found: ${templateIdentifier}`);
|
|
||||||
throw new CommandError(
|
|
||||||
"template.export.not_found",
|
|
||||||
`No template found: ${templateIdentifier}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize the template without indentation to preserve the engine output shape.
|
|
||||||
const serializedTemplate = JSON.stringify(rawTemplate);
|
|
||||||
|
|
||||||
// Resolve output file from --output (or -o), then fallback to optional positional output file
|
|
||||||
const outputFile = options["output"] ?? args[1];
|
|
||||||
|
|
||||||
// If no output file is provided, print the template to stdout
|
|
||||||
if (!outputFile) {
|
|
||||||
deps.io.out(serializedTemplate);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve output file path and write the template to disk
|
|
||||||
const outputPath = path.resolve(process.cwd(), outputFile);
|
|
||||||
try {
|
|
||||||
writeFileSync(outputPath, serializedTemplate);
|
|
||||||
} catch (error) {
|
|
||||||
throw new CommandError(
|
|
||||||
"template.export.write_failed",
|
|
||||||
`Failed to export template to file: ${outputPath} (${error instanceof Error ? error.message : "unknown error"})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
deps.io.out(`Template exported to: ${outputPath}`);
|
|
||||||
return { outputFile: outputPath };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the template command.
|
* Handles the template command.
|
||||||
* Throws CommandError on failure, returns result data on success.
|
* Throws CommandError on failure, returns result data on success.
|
||||||
@@ -417,8 +348,8 @@ export const handleTemplateExportCommand = async (
|
|||||||
export const handleTemplateCommand = async (
|
export const handleTemplateCommand = async (
|
||||||
deps: CommandDependencies,
|
deps: CommandDependencies,
|
||||||
args: string[],
|
args: string[],
|
||||||
options: Record<string, string>,
|
_options: Record<string, string>,
|
||||||
): Promise<{ templateFile?: string; count?: number; outputFile?: string }> => {
|
): Promise<{ templateFile?: string; count?: number }> => {
|
||||||
// Get the sub-command from the arguments
|
// Get the sub-command from the arguments
|
||||||
const subCommand = args[0];
|
const subCommand = args[0];
|
||||||
|
|
||||||
@@ -483,10 +414,6 @@ export const handleTemplateCommand = async (
|
|||||||
// Handle the template inspect command, We offload here as it has lots of arguments and is quite long
|
// Handle the template inspect command, We offload here as it has lots of arguments and is quite long
|
||||||
return handleTemplateInspectCommand(deps, args.slice(1));
|
return handleTemplateInspectCommand(deps, args.slice(1));
|
||||||
}
|
}
|
||||||
case "export": {
|
|
||||||
// Handle the template export command
|
|
||||||
return handleTemplateExportCommand(deps, args.slice(1), options);
|
|
||||||
}
|
|
||||||
case "set-default": {
|
case "set-default": {
|
||||||
// Get the template file, output identifier, and role identifier from the arguments
|
// Get the template file, output identifier, and role identifier from the arguments
|
||||||
const templateFile = args[1];
|
const templateFile = args[1];
|
||||||
|
|||||||
@@ -81,7 +81,22 @@ export class AppService extends EventEmitter<AppEventMap> {
|
|||||||
|
|
||||||
// TODO: We *technically* dont want this here, but we also need some initial templates for the wallet, so im doing it here
|
// TODO: We *technically* dont want this here, but we also need some initial templates for the wallet, so im doing it here
|
||||||
// Import the default P2PKH template
|
// Import the default P2PKH template
|
||||||
await engine.importTemplate(p2pkhTemplate);
|
const { templateIdentifier } = await engine.importTemplate(p2pkhTemplate);
|
||||||
|
|
||||||
|
// engine
|
||||||
|
// .subscribeToLockingBytecodesForTemplate(templateIdentifier)
|
||||||
|
// .catch((err) =>
|
||||||
|
// console.error(
|
||||||
|
// `Error subscribing to locking bytecodes for template ${templateIdentifier}: ${err}`,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// engine
|
||||||
|
// .updateUnspentOutputsForTemplate(templateIdentifier)
|
||||||
|
// .catch((err) =>
|
||||||
|
// console.error(
|
||||||
|
// `Error updating unspent outputs for template ${templateIdentifier}: ${err}`,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
|
||||||
// Update all the unspents for every template, and subscribe to the locking bytecodes for changes
|
// Update all the unspents for every template, and subscribe to the locking bytecodes for changes
|
||||||
// TODO: Remove the above lines that do the same thing. Minimising changes for BLISS.
|
// TODO: Remove the above lines that do the same thing. Minimising changes for BLISS.
|
||||||
@@ -89,8 +104,8 @@ export class AppService extends EventEmitter<AppEventMap> {
|
|||||||
const templates = await engine.listImportedTemplates();
|
const templates = await engine.listImportedTemplates();
|
||||||
|
|
||||||
templates.forEach(async (template) => {
|
templates.forEach(async (template) => {
|
||||||
engine.updateUnspentOutputsForTemplate(generateTemplateIdentifier(template));
|
// engine.updateUnspentOutputsForTemplate(generateTemplateIdentifier(template));
|
||||||
engine.subscribeToScriptHashForTemplate(generateTemplateIdentifier(template));
|
engine.subscribeToLockingBytecodesForTemplate(generateTemplateIdentifier(template));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { binToHex, hexToBin, sha256 } from "@bitauth/libauth";
|
import { binToHex, hexToBin, sha256 } from "@bitauth/libauth";
|
||||||
import { compileCashAssemblyString, type Engine } from "@xo-cash/engine";
|
import { compileCashAssemblyString, type Engine } from "@xo-cash/engine";
|
||||||
import type { ScriptHashData, State, UnspentOutputData } from "@xo-cash/state";
|
import type { ScriptHashData, UnspentOutputData } from "@xo-cash/state";
|
||||||
import type {
|
import type {
|
||||||
XOInvitation,
|
XOInvitation,
|
||||||
XOInvitationCommit,
|
XOInvitationCommit,
|
||||||
@@ -146,10 +146,8 @@ export class HistoryService {
|
|||||||
const contexts = new Map<string, InvitationContext>();
|
const contexts = new Map<string, InvitationContext>();
|
||||||
|
|
||||||
for (const invitation of this.invitations) {
|
for (const invitation of this.invitations) {
|
||||||
const templateIdentifier = invitation.data.templateIdentifier;
|
const template =
|
||||||
const template = templateIdentifier
|
(await this.engine.getTemplate(invitation.data.templateIdentifier)) ?? null;
|
||||||
? (await this.engine.getTemplate(templateIdentifier)) ?? null
|
|
||||||
: null;
|
|
||||||
contexts.set(invitation.data.invitationIdentifier, {
|
contexts.set(invitation.data.invitationIdentifier, {
|
||||||
invitation,
|
invitation,
|
||||||
template,
|
template,
|
||||||
@@ -166,19 +164,11 @@ export class HistoryService {
|
|||||||
const scriptHashDataByScriptHash = new Map<string, ScriptHashData>();
|
const scriptHashDataByScriptHash = new Map<string, ScriptHashData>();
|
||||||
const templateIdentifiers = new Set<string>();
|
const templateIdentifiers = new Set<string>();
|
||||||
|
|
||||||
for (const invitation of this.invitations) {
|
for (const utxo of allUtxos) {
|
||||||
if (invitation.data.templateIdentifier) {
|
templateIdentifiers.add(utxo.templateIdentifier);
|
||||||
templateIdentifiers.add(invitation.data.templateIdentifier);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
for (const invitation of this.invitations) {
|
||||||
const uniqueScriptHashes = new Set(allUtxos.map((utxo) => utxo.scriptHash));
|
templateIdentifiers.add(invitation.data.templateIdentifier);
|
||||||
for (const scriptHash of uniqueScriptHashes) {
|
|
||||||
const scriptHashData = await this.getScriptHashData(scriptHash);
|
|
||||||
if (scriptHashData === undefined) continue;
|
|
||||||
|
|
||||||
scriptHashDataByScriptHash.set(scriptHash, scriptHashData);
|
|
||||||
templateIdentifiers.add(scriptHashData.templateIdentifier);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const templateIdentifier of templateIdentifiers) {
|
for (const templateIdentifier of templateIdentifiers) {
|
||||||
@@ -196,10 +186,8 @@ export class HistoryService {
|
|||||||
metadataIndex: WalletMetadataIndex,
|
metadataIndex: WalletMetadataIndex,
|
||||||
): Promise<UtxoContext> {
|
): Promise<UtxoContext> {
|
||||||
const scriptHashData = metadataIndex.scriptHashDataByScriptHash.get(utxo.scriptHash);
|
const scriptHashData = metadataIndex.scriptHashDataByScriptHash.get(utxo.scriptHash);
|
||||||
const templateIdentifier = scriptHashData?.templateIdentifier;
|
const templateIdentifier = scriptHashData?.templateIdentifier ?? utxo.templateIdentifier;
|
||||||
const template = templateIdentifier
|
const template = (await this.engine.getTemplate(templateIdentifier)) ?? null;
|
||||||
? (await this.engine.getTemplate(templateIdentifier)) ?? null
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
utxo,
|
utxo,
|
||||||
@@ -311,7 +299,7 @@ export class HistoryService {
|
|||||||
if (!matchingContext) continue;
|
if (!matchingContext) continue;
|
||||||
|
|
||||||
const lockingBytecode = this.getOutputLockingBytecodeHex(output) ?? matchingContext.scriptHashData?.lockingBytecode;
|
const lockingBytecode = this.getOutputLockingBytecodeHex(output) ?? matchingContext.scriptHashData?.lockingBytecode;
|
||||||
const outputIdentifier = output.outputIdentifier ?? matchingContext.scriptHashData?.outputIdentifier;
|
const outputIdentifier = output.outputIdentifier ?? matchingContext.scriptHashData?.outputIdentifier ?? matchingContext.utxo.outputIdentifier;
|
||||||
const role =
|
const role =
|
||||||
output.roleIdentifier ??
|
output.roleIdentifier ??
|
||||||
this.getFirstEntityRole(entityRoles, commit.entityIdentifier) ??
|
this.getFirstEntityRole(entityRoles, commit.entityIdentifier) ??
|
||||||
@@ -392,21 +380,20 @@ export class HistoryService {
|
|||||||
if (usedUtxoIds.has(this.getUtxoId(context.utxo))) return false;
|
if (usedUtxoIds.has(this.getUtxoId(context.utxo))) return false;
|
||||||
if (scriptHash && context.utxo.scriptHash === scriptHash) return true;
|
if (scriptHash && context.utxo.scriptHash === scriptHash) return true;
|
||||||
if (lockingBytecode && context.scriptHashData?.lockingBytecode === lockingBytecode) return true;
|
if (lockingBytecode && context.scriptHashData?.lockingBytecode === lockingBytecode) return true;
|
||||||
|
if (output.outputIdentifier && context.utxo.outputIdentifier === output.outputIdentifier) return true;
|
||||||
if (output.outputIdentifier && context.scriptHashData?.outputIdentifier === output.outputIdentifier) return true;
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private projectStandaloneUtxo(context: UtxoContext): WalletHistoryItem {
|
private projectStandaloneUtxo(context: UtxoContext): WalletHistoryItem {
|
||||||
const output = this.projectUtxoOutput(context);
|
const output = this.projectUtxoOutput(context);
|
||||||
const templateIdentifier = context.scriptHashData?.templateIdentifier;
|
const templateIdentifier = context.scriptHashData?.templateIdentifier ?? context.utxo.templateIdentifier;
|
||||||
const role = output.role;
|
const role = output.role;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `utxo-${context.utxo.outpointTransactionHash}:${context.utxo.outpointIndex}`,
|
id: `utxo-${context.utxo.outpointTransactionHash}:${context.utxo.outpointIndex}`,
|
||||||
source: "utxo",
|
source: "utxo",
|
||||||
templateIdentifier: templateIdentifier ?? "",
|
templateIdentifier,
|
||||||
template: context.template?.name ?? "UnknownTemplate",
|
template: context.template?.name ?? "UnknownTemplate",
|
||||||
roles: role ? [role] : ["unknown"],
|
roles: role ? [role] : ["unknown"],
|
||||||
description: output.description,
|
description: output.description,
|
||||||
@@ -417,7 +404,7 @@ export class HistoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private projectUtxoOutput(context: UtxoContext): WalletHistoryOutput {
|
private projectUtxoOutput(context: UtxoContext): WalletHistoryOutput {
|
||||||
const outputIdentifier = context.scriptHashData?.outputIdentifier;
|
const outputIdentifier = context.scriptHashData?.outputIdentifier ?? context.utxo.outputIdentifier;
|
||||||
const role = context.scriptHashData?.roleIdentifier;
|
const role = context.scriptHashData?.roleIdentifier;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -570,7 +557,7 @@ export class HistoryService {
|
|||||||
variables: Record<string, XOInvitationVariableValue>,
|
variables: Record<string, XOInvitationVariableValue>,
|
||||||
): string {
|
): string {
|
||||||
try {
|
try {
|
||||||
return compileCashAssemblyString({ cashAssemblyText: description, variables, evaluationDecodeMode: 'utf8' });
|
return compileCashAssemblyString(description, variables);
|
||||||
} catch {
|
} catch {
|
||||||
return this.interpolateSimpleCashAssemblyVariables(description, variables);
|
return this.interpolateSimpleCashAssemblyVariables(description, variables);
|
||||||
}
|
}
|
||||||
@@ -604,10 +591,6 @@ export class HistoryService {
|
|||||||
: binToHex(output.lockingBytecode);
|
: binToHex(output.lockingBytecode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getScriptHashData(scriptHash: string): Promise<ScriptHashData | undefined> {
|
|
||||||
return (this.engine as unknown as { state: State }).state.getScriptHashData(scriptHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getOutpointKey(txid: string, index: number): string {
|
private getOutpointKey(txid: string, index: number): string {
|
||||||
return `${txid}:${index}`;
|
return `${txid}:${index}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ import type {
|
|||||||
} from "@xo-cash/types";
|
} from "@xo-cash/types";
|
||||||
import type { UnspentOutputData } from "@xo-cash/state";
|
import type { UnspentOutputData } from "@xo-cash/state";
|
||||||
import {
|
import {
|
||||||
bigIntToBinUint64LE,
|
|
||||||
bigIntToBinUintBE,
|
|
||||||
bigIntToBinUintLE,
|
|
||||||
bigIntToVmNumber,
|
|
||||||
binToHex,
|
binToHex,
|
||||||
encodeTransaction,
|
encodeTransaction,
|
||||||
generateTransaction,
|
generateTransaction,
|
||||||
@@ -154,10 +150,6 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
* Start the invitation - Connect sync server and download latest invitation data.
|
* Start the invitation - Connect sync server and download latest invitation data.
|
||||||
*/
|
*/
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
// Persist immediately so imports survive sync-server outages and appear in the TUI
|
|
||||||
// after a CLI import or app restart.
|
|
||||||
await this.storage.set(this.data.invitationIdentifier, this.data);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Connect to the sync server and get the invitation (in parallel)
|
// Connect to the sync server and get the invitation (in parallel)
|
||||||
const [_, invitation] = await Promise.all([
|
const [_, invitation] = await Promise.all([
|
||||||
@@ -287,8 +279,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
private async computeStatusInternal(): Promise<string> {
|
private async computeStatusInternal(): Promise<string> {
|
||||||
let missingReqs;
|
let missingReqs;
|
||||||
try {
|
try {
|
||||||
const missingRequirements = await this.engine.listMissingRequirements(this.data.invitationIdentifier);
|
missingReqs = await this.engine.listMissingRequirements(this.data);
|
||||||
missingReqs = missingRequirements.templateRequirements;
|
|
||||||
} catch {
|
} catch {
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
@@ -403,7 +394,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
*/
|
*/
|
||||||
async sign(): Promise<void> {
|
async sign(): Promise<void> {
|
||||||
// Sign the invitation
|
// Sign the invitation
|
||||||
const signedInvitation = await this.engine.signInvitation(this.data.invitationIdentifier);
|
const signedInvitation = await this.engine.signInvitation(this.data);
|
||||||
|
|
||||||
// Publish the signed invitation to the sync server
|
// Publish the signed invitation to the sync server
|
||||||
this.publishInvitation(signedInvitation);
|
this.publishInvitation(signedInvitation);
|
||||||
@@ -422,7 +413,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
* @returns The transaction hash returned by the network after broadcast.
|
* @returns The transaction hash returned by the network after broadcast.
|
||||||
*/
|
*/
|
||||||
async broadcast(): Promise<string> {
|
async broadcast(): Promise<string> {
|
||||||
const txHash = await this.engine.executeAction(this.data.invitationIdentifier, {
|
const txHash = await this.engine.executeAction(this.data, {
|
||||||
broadcastTransaction: true,
|
broadcastTransaction: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -440,7 +431,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
*/
|
*/
|
||||||
async append(data: AppendInvitationParameters): Promise<void> {
|
async append(data: AppendInvitationParameters): Promise<void> {
|
||||||
// Append the commit to the invitation
|
// Append the commit to the invitation
|
||||||
this.data = await this.engine.appendInvitation(this.data.invitationIdentifier, data);
|
this.data = await this.engine.appendInvitation(this.data, data);
|
||||||
|
|
||||||
// Sync the invitation to the sync server
|
// Sync the invitation to the sync server
|
||||||
await this.publishInvitation(this.data);
|
await this.publishInvitation(this.data);
|
||||||
@@ -555,7 +546,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
* Get the missing requirements for the invitation
|
* Get the missing requirements for the invitation
|
||||||
*/
|
*/
|
||||||
async getMissingRequirements() {
|
async getMissingRequirements() {
|
||||||
return this.engine.listMissingRequirements(this.data.invitationIdentifier);
|
return this.engine.listMissingRequirements(this.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -617,41 +608,33 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueSatoshisExpression = output.valueSatoshis;
|
const valueSatoshisIdentifier = output.valueSatoshis;
|
||||||
if (!valueSatoshisExpression) {
|
if (!valueSatoshisIdentifier) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Value satoshis identifier not found: ${outputIdentifier} in template: ${this.data.templateIdentifier}`,
|
`Value satoshis identifier not found: ${outputIdentifier} in template: ${this.data.templateIdentifier}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.dir(this.data, { depth: null });
|
|
||||||
|
|
||||||
// Create a list of all the variables from the commits
|
// Create a list of all the variables from the commits
|
||||||
const variables = this.data.commits.flatMap(
|
const variables = this.data.commits.flatMap(
|
||||||
(c) => c.data?.variables ?? [],
|
(c) => c.data?.variables ?? [],
|
||||||
);
|
);
|
||||||
console.dir(variables, { depth: null });
|
|
||||||
|
|
||||||
// Create a dictionary of the variables
|
// Create a dictionary of the variables
|
||||||
const formattedVariables = variables.reduce(
|
const formattedVariables = variables.reduce(
|
||||||
(acc, v) => {
|
(acc, v) => {
|
||||||
const { variableIdentifier, value } = v;
|
acc[v.variableIdentifier ?? ""] = v.value;
|
||||||
console.log(typeof value);
|
|
||||||
acc[variableIdentifier ?? ""] = value;
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<string, XOInvitationVariableValue>,
|
{} as Record<string, XOInvitationVariableValue>,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.dir(formattedVariables, { depth: null });
|
|
||||||
|
|
||||||
// Compile the CashAssembly expression to get the value satoshis (It handles the variable replacement for us)
|
// Compile the CashAssembly expression to get the value satoshis (It handles the variable replacement for us)
|
||||||
const valueSatoshis = compileCashAssemblyString(
|
const valueSatoshis = await compileCashAssemblyString(
|
||||||
{ cashAssemblyText: String(valueSatoshisExpression), variables: formattedVariables, evaluationDecodeMode: 'bigint' },
|
String(valueSatoshisIdentifier),
|
||||||
|
formattedVariables,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.dir(valueSatoshis, { depth: null });
|
|
||||||
|
|
||||||
// Return the value satoshis as a bigint
|
// Return the value satoshis as a bigint
|
||||||
// TODO: Check this of a vulnerability or crash - I assume there might be one if someone made the `valueSatoshis` a malicious expression
|
// TODO: Check this of a vulnerability or crash - I assume there might be one if someone made the `valueSatoshis` a malicious expression
|
||||||
return BigInt(valueSatoshis);
|
return BigInt(valueSatoshis);
|
||||||
@@ -705,13 +688,9 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
// Iterate through the outputs and sum the valueSatoshis
|
// Iterate through the outputs and sum the valueSatoshis
|
||||||
for (const output of outputs) {
|
for (const output of outputs) {
|
||||||
if (typeof output === "string") {
|
if (typeof output === "string") {
|
||||||
const sats = await this.getSatsOut(output);
|
totalSats += await this.getSatsOut(output);
|
||||||
console.log(`Sats for output: ${output} is ${sats}`);
|
|
||||||
totalSats += sats
|
|
||||||
} else {
|
} else {
|
||||||
const sats = await this.getSatsOut(output.output);
|
totalSats += await this.getSatsOut(output.output);
|
||||||
console.log(`Sats for output: ${output.output} is ${sats}`);
|
|
||||||
totalSats += sats;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Database from "better-sqlite3";
|
// import Database from "better-sqlite3";
|
||||||
import { deserializeInvitation, serializeInvitation } from "@xo-cash/engine";
|
import { decodeExtendedJson, encodeExtendedJson } from "../utils/ext-json.js";
|
||||||
import type { XOInvitation } from "@xo-cash/types";
|
|
||||||
|
import { DatabaseSync } from 'node:sqlite'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is not an actual storage adapter that we want to make use of. This storage adapter is a stop-gap while the engine is under development.
|
* This is not an actual storage adapter that we want to make use of. This storage adapter is a stop-gap while the engine is under development.
|
||||||
@@ -22,7 +23,7 @@ export abstract class BaseStorage {
|
|||||||
export class Storage extends BaseStorage {
|
export class Storage extends BaseStorage {
|
||||||
static async create(dbPath: string): Promise<Storage> {
|
static async create(dbPath: string): Promise<Storage> {
|
||||||
// Create the database
|
// Create the database
|
||||||
const database = new Database(dbPath);
|
const database = new DatabaseSync(dbPath);
|
||||||
|
|
||||||
// Create the storage table if it doesn't exist
|
// Create the storage table if it doesn't exist
|
||||||
database
|
database
|
||||||
@@ -35,7 +36,7 @@ export class Storage extends BaseStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly database: Database.Database,
|
private readonly database: DatabaseSync,
|
||||||
private readonly basePath: string,
|
private readonly basePath: string,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
@@ -57,8 +58,9 @@ export class Storage extends BaseStorage {
|
|||||||
return fullKey.startsWith(prefix) ? fullKey.slice(prefix.length) : fullKey;
|
return fullKey.startsWith(prefix) ? fullKey.slice(prefix.length) : fullKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async set(key: string, value: XOInvitation): Promise<void> {
|
async set(key: string, value: any): Promise<void> {
|
||||||
const encodedValue = serializeInvitation(value);
|
// Encode the extended json object
|
||||||
|
const encodedValue = encodeExtendedJson(value);
|
||||||
|
|
||||||
// Insert or replace the value into the database with full key (including basePath)
|
// Insert or replace the value into the database with full key (including basePath)
|
||||||
const fullKey = this.getFullKey(key);
|
const fullKey = this.getFullKey(key);
|
||||||
@@ -93,10 +95,10 @@ export class Storage extends BaseStorage {
|
|||||||
return !strippedKey.includes(".");
|
return !strippedKey.includes(".");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Deserialize invitations and strip basePath from keys
|
// Decode the extended json objects and strip basePath from keys
|
||||||
return filteredRows.map((row) => ({
|
return filteredRows.map((row) => ({
|
||||||
key: this.stripBasePath(row.key),
|
key: this.stripBasePath(row.key),
|
||||||
value: deserializeInvitation(row.value),
|
value: decodeExtendedJson(row.value),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +113,7 @@ export class Storage extends BaseStorage {
|
|||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
|
|
||||||
// Decode the extended json object
|
// Decode the extended json object
|
||||||
return deserializeInvitation(row.value);
|
return decodeExtendedJson(row.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(key: string): Promise<void> {
|
async remove(key: string): Promise<void> {
|
||||||
@@ -174,9 +176,9 @@ export class InMemoryStorage extends BaseStorage {
|
|||||||
return fullKey.startsWith(prefix) ? fullKey.slice(prefix.length) : fullKey;
|
return fullKey.startsWith(prefix) ? fullKey.slice(prefix.length) : fullKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async set(key: string, value: XOInvitation): Promise<void> {
|
async set(key: string, value: any): Promise<void> {
|
||||||
const fullKey = this.getFullKey(key);
|
const fullKey = this.getFullKey(key);
|
||||||
const encodedValue = serializeInvitation(value);
|
const encodedValue = encodeExtendedJson(value);
|
||||||
this.store.set(fullKey, encodedValue);
|
this.store.set(fullKey, encodedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +201,7 @@ export class InMemoryStorage extends BaseStorage {
|
|||||||
|
|
||||||
return filteredRows.map((row) => ({
|
return filteredRows.map((row) => ({
|
||||||
key: this.stripBasePath(row.key),
|
key: this.stripBasePath(row.key),
|
||||||
value: deserializeInvitation(row.value),
|
value: decodeExtendedJson(row.value),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +210,7 @@ export class InMemoryStorage extends BaseStorage {
|
|||||||
const encodedValue = this.store.get(fullKey);
|
const encodedValue = this.store.get(fullKey);
|
||||||
if (encodedValue === undefined) return null;
|
if (encodedValue === undefined) return null;
|
||||||
|
|
||||||
return deserializeInvitation(encodedValue);
|
return decodeExtendedJson(encodedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(key: string): Promise<void> {
|
async remove(key: string): Promise<void> {
|
||||||
|
|||||||
1435
src/templates/p2pkh.ts
Normal file
1435
src/templates/p2pkh.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,6 @@ import {
|
|||||||
formatActionListItem,
|
formatActionListItem,
|
||||||
getTemplateRoles,
|
getTemplateRoles,
|
||||||
} from '../../utils/template-utils.js';
|
} from '../../utils/template-utils.js';
|
||||||
import { buildScriptHashDataMap } from '../../utils/utxo-metadata.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Template item with metadata.
|
* Template item with metadata.
|
||||||
@@ -84,21 +83,12 @@ export function TemplateListScreen(): React.ReactElement {
|
|||||||
|
|
||||||
const templateList = await appService.engine.listImportedTemplates();
|
const templateList = await appService.engine.listImportedTemplates();
|
||||||
const allUtxos = await appService.engine.listUnspentOutputsData();
|
const allUtxos = await appService.engine.listUnspentOutputsData();
|
||||||
const scriptHashDataByScriptHash =
|
|
||||||
await buildScriptHashDataMap(appService.engine);
|
|
||||||
|
|
||||||
const ownedOutputsByTemplate = new Map<string, Set<string>>();
|
const ownedOutputsByTemplate = new Map<string, Set<string>>();
|
||||||
for (const utxo of allUtxos) {
|
for (const utxo of allUtxos) {
|
||||||
const scriptRow = scriptHashDataByScriptHash.get(utxo.scriptHash);
|
const existing = ownedOutputsByTemplate.get(utxo.templateIdentifier) ?? new Set<string>();
|
||||||
if (scriptRow === undefined) {
|
existing.add(utxo.outputIdentifier);
|
||||||
continue;
|
ownedOutputsByTemplate.set(utxo.templateIdentifier, existing);
|
||||||
}
|
|
||||||
|
|
||||||
const existing =
|
|
||||||
ownedOutputsByTemplate.get(scriptRow.templateIdentifier) ??
|
|
||||||
new Set<string>();
|
|
||||||
existing.add(scriptRow.outputIdentifier);
|
|
||||||
ownedOutputsByTemplate.set(scriptRow.templateIdentifier, existing);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadedTemplates = await Promise.all(
|
const loadedTemplates = await Promise.all(
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
|
|
||||||
let isCurrent = true;
|
let isCurrent = true;
|
||||||
|
|
||||||
appService.engine.findOwnCommits(selectedInvitation.data.invitationIdentifier)
|
appService.engine.getOwnCommits(selectedInvitation.data)
|
||||||
.then((ownCommits) => {
|
.then((ownCommits) => {
|
||||||
if (!isCurrent) return;
|
if (!isCurrent) return;
|
||||||
|
|
||||||
@@ -723,14 +723,10 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
{outputTemplate?.name ?? output.outputIdentifier ?? `Output ${idx}`}
|
{outputTemplate?.name ?? output.outputIdentifier ?? `Output ${idx}`}
|
||||||
|
|
||||||
{/* Output description */}
|
{/* Output description */}
|
||||||
{outputTemplate?.description && ' - ' + compileCashAssemblyString({
|
{outputTemplate?.description && ' - ' + compileCashAssemblyString(outputTemplate?.description ?? '', variables.reduce((acc, variable) => {
|
||||||
cashAssemblyText: outputTemplate?.description,
|
acc[variable.variableIdentifier] = variable.value as XOInvitationVariableValue;
|
||||||
variables: variables.reduce((acc, variable) => {
|
return acc;
|
||||||
acc[variable.variableIdentifier] = variable.value as XOInvitationVariableValue;
|
}, {} as Record<string, XOInvitationVariableValue>))}
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, XOInvitationVariableValue>)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Output value */}
|
{/* Output value */}
|
||||||
{outputSatoshis !== null && ` (${formatSatoshis(outputSatoshis)}${getFiatSuffix(outputSatoshis)})`}
|
{outputSatoshis !== null && ` (${formatSatoshis(outputSatoshis)}${getFiatSuffix(outputSatoshis)})`}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const isInvitationRequirementsComplete = async (
|
|||||||
invitation: Invitation,
|
invitation: Invitation,
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
const missingRequirements = await invitation.getMissingRequirements();
|
const missingRequirements = await invitation.getMissingRequirements();
|
||||||
return !hasMissingRequirements(missingRequirements.templateRequirements);
|
return !hasMissingRequirements(missingRequirements);
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Move to engine in templates.ts
|
// TODO: Move to engine in templates.ts
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { XOInvitation } from "@xo-cash/types";
|
import type { XOInvitation } from "@xo-cash/types";
|
||||||
import { EventEmitter } from "./event-emitter.js";
|
import { EventEmitter } from "./event-emitter.js";
|
||||||
import { SSESession, type SSEvent } from "./sse-client.js";
|
import { SSESession, type SSEvent } from "./sse-client.js";
|
||||||
import { deserializeInvitation, serializeInvitation } from "@xo-cash/engine";
|
import { decodeExtendedJson, encodeExtendedJson } from "./ext-json.js";
|
||||||
|
|
||||||
export type SyncServerEventMap = {
|
export type SyncServerEventMap = {
|
||||||
connected: void;
|
connected: void;
|
||||||
@@ -81,7 +81,9 @@ export class SyncServer extends EventEmitter<SyncServerEventMap> {
|
|||||||
throw new Error(`Failed to get invitation: ${response.statusText}`);
|
throw new Error(`Failed to get invitation: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const invitation = deserializeInvitation(await response.text());
|
const invitation = decodeExtendedJson(await response.text()) as
|
||||||
|
| XOInvitation
|
||||||
|
| undefined;
|
||||||
return invitation;
|
return invitation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +96,7 @@ export class SyncServer extends EventEmitter<SyncServerEventMap> {
|
|||||||
// Send a POST request to the sync server
|
// Send a POST request to the sync server
|
||||||
const response = await fetch(`${this.baseUrl}/invitations`, {
|
const response = await fetch(`${this.baseUrl}/invitations`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: serializeInvitation(invitation),
|
body: encodeExtendedJson(invitation),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
@@ -107,7 +109,7 @@ export class SyncServer extends EventEmitter<SyncServerEventMap> {
|
|||||||
|
|
||||||
// Read the returned JSON
|
// Read the returned JSON
|
||||||
// TODO: This should use zod to verify the response
|
// TODO: This should use zod to verify the response
|
||||||
const data = deserializeInvitation(await response.text());
|
const data = decodeExtendedJson(await response.text()) as XOInvitation;
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import type { Engine } from "@xo-cash/engine";
|
|
||||||
import type { ScriptHashData, UnspentOutputData } from "@xo-cash/state";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Template and output identifiers resolved from script hash storage.
|
|
||||||
*/
|
|
||||||
export type UnspentOutputMetadata = {
|
|
||||||
templateIdentifier?: string;
|
|
||||||
outputIdentifier?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UnspentOutputWithMetadata = UnspentOutputData & UnspentOutputMetadata;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a lookup map from script hash to its stored metadata.
|
|
||||||
*/
|
|
||||||
export const buildScriptHashDataMap = async (
|
|
||||||
engine: Engine,
|
|
||||||
): Promise<Map<string, ScriptHashData>> => {
|
|
||||||
const scriptHashes = await engine.listScriptHashes();
|
|
||||||
const scriptHashDataByScriptHash = new Map<string, ScriptHashData>();
|
|
||||||
|
|
||||||
for (const scriptHashRow of scriptHashes) {
|
|
||||||
scriptHashDataByScriptHash.set(scriptHashRow.scriptHash, scriptHashRow);
|
|
||||||
}
|
|
||||||
|
|
||||||
return scriptHashDataByScriptHash;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves template/output metadata for a single UTXO via its script hash.
|
|
||||||
*/
|
|
||||||
export const getUnspentOutputMetadata = (
|
|
||||||
utxo: UnspentOutputData,
|
|
||||||
scriptHashDataByScriptHash: Map<string, ScriptHashData>,
|
|
||||||
): UnspentOutputMetadata => {
|
|
||||||
const scriptRow = scriptHashDataByScriptHash.get(utxo.scriptHash);
|
|
||||||
|
|
||||||
if (scriptRow === undefined) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
templateIdentifier: scriptRow.templateIdentifier,
|
|
||||||
outputIdentifier: scriptRow.outputIdentifier,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a UTXO enriched with template/output metadata from script hash storage.
|
|
||||||
*/
|
|
||||||
export const enrichUnspentOutput = (
|
|
||||||
utxo: UnspentOutputData,
|
|
||||||
scriptHashDataByScriptHash: Map<string, ScriptHashData>,
|
|
||||||
): UnspentOutputWithMetadata => ({
|
|
||||||
...utxo,
|
|
||||||
...getUnspentOutputMetadata(utxo, scriptHashDataByScriptHash),
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { expect, test, describe, beforeEach, afterEach } from "vitest";
|
import { expect, test, describe, beforeEach, afterEach } from "vitest";
|
||||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
@@ -98,13 +98,6 @@ const testCases: TestCase[] = [
|
|||||||
shouldThrow: false,
|
shouldThrow: false,
|
||||||
expectedData: {},
|
expectedData: {},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "export returns raw template json to stdout",
|
|
||||||
inputs: ["export", p2pkhTemplateIdentifier],
|
|
||||||
shouldThrow: false,
|
|
||||||
expectedData: {},
|
|
||||||
logs: [{ out: "\"name\":\"Wallet (P2PKH)\"" }],
|
|
||||||
},
|
|
||||||
// Error cases - subcommand
|
// Error cases - subcommand
|
||||||
{
|
{
|
||||||
name: "throws when no subcommand provided",
|
name: "throws when no subcommand provided",
|
||||||
@@ -131,18 +124,6 @@ const testCases: TestCase[] = [
|
|||||||
shouldThrow: true,
|
shouldThrow: true,
|
||||||
expectedEvent: "template.import.file_not_found",
|
expectedEvent: "template.import.file_not_found",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "throws when export called without template identifier",
|
|
||||||
inputs: ["export"],
|
|
||||||
shouldThrow: true,
|
|
||||||
expectedEvent: "template.export.identifier_missing",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "throws when export called with unknown template",
|
|
||||||
inputs: ["export", "unknown-template"],
|
|
||||||
shouldThrow: true,
|
|
||||||
expectedEvent: "template.export.not_found",
|
|
||||||
},
|
|
||||||
// Error cases - list category
|
// Error cases - list category
|
||||||
{
|
{
|
||||||
name: "throws when list category called without template identifier",
|
name: "throws when list category called without template identifier",
|
||||||
@@ -282,42 +263,4 @@ describe("template command", () => {
|
|||||||
process.chdir(originalCwd);
|
process.chdir(originalCwd);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("export prints exact engine template JSON to stdout", async () => {
|
|
||||||
const { io, capture } = createMockIO();
|
|
||||||
const expectedTemplate = await engine.getTemplate(p2pkhTemplateIdentifier);
|
|
||||||
expect(expectedTemplate).toBeDefined();
|
|
||||||
|
|
||||||
await handleTemplateCommand(
|
|
||||||
createCommandDeps(app, io),
|
|
||||||
["export", p2pkhTemplateIdentifier],
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(capture.out[0]).toBe(JSON.stringify(expectedTemplate));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("export writes exact engine template JSON to file", async () => {
|
|
||||||
const outputFile = "exported-template.json";
|
|
||||||
const outputPath = path.join(tempDir, outputFile);
|
|
||||||
const { io } = createMockIO();
|
|
||||||
const expectedTemplate = await engine.getTemplate(p2pkhTemplateIdentifier);
|
|
||||||
expect(expectedTemplate).toBeDefined();
|
|
||||||
|
|
||||||
const originalCwd = process.cwd();
|
|
||||||
process.chdir(tempDir);
|
|
||||||
try {
|
|
||||||
const result = await handleTemplateCommand(
|
|
||||||
createCommandDeps(app, io),
|
|
||||||
["export", p2pkhTemplateIdentifier],
|
|
||||||
{ output: outputFile },
|
|
||||||
);
|
|
||||||
|
|
||||||
const exportedTemplate = readFileSync(outputPath, "utf8");
|
|
||||||
expect(exportedTemplate).toBe(JSON.stringify(expectedTemplate));
|
|
||||||
expect(result.outputFile).toBe(path.resolve(process.cwd(), outputFile));
|
|
||||||
} finally {
|
|
||||||
process.chdir(originalCwd);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ import {
|
|||||||
import { BCHMnemonicURL } from "../../src/utils/bch-mnemonic-url";
|
import { BCHMnemonicURL } from "../../src/utils/bch-mnemonic-url";
|
||||||
|
|
||||||
const TEST_SEED =
|
const TEST_SEED =
|
||||||
"page pencil stock planet limb cluster assault speak off joke private pioneer";
|
"oven crop same above under tower promote decrease vocal pretty require slow";
|
||||||
|
|
||||||
describe("mnemonic utilities", () => {
|
describe("mnemonic utilities", () => {
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
|
|
||||||
@@ -54,7 +53,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-page$/);
|
expect(filename).toMatch(/^mnemonic-oven$/);
|
||||||
expect(existsSync(path.join(tempDir, filename))).toBe(true);
|
expect(existsSync(path.join(tempDir, filename))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
// 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";
|
||||||
@@ -12,13 +13,15 @@ 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 =
|
||||||
"page pencil stock planet limb cluster assault speak off joke private pioneer";
|
"oven crop same above under tower promote decrease vocal pretty require slow";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for creating a fake resource (UTXO) in tests.
|
* Options for creating a fake resource (UTXO) in tests.
|
||||||
@@ -67,8 +70,6 @@ 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,
|
||||||
@@ -130,10 +131,7 @@ 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 = await createStorageAdapter({
|
const storage = new XOStorageInMemory();
|
||||||
storageType: "inmemory",
|
|
||||||
accountHash: binToHex(sha256.hash(convertMnemonicToSeedBytes(seed))),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize the storage adapter.
|
// Initialize the storage adapter.
|
||||||
await storage.initialize();
|
await storage.initialize();
|
||||||
@@ -143,10 +141,7 @@ 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);
|
||||||
@@ -160,10 +155,15 @@ 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);
|
const rates = new RatesService(mockRates, settings);
|
||||||
|
|
||||||
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);
|
return new AppService(engine, storage, config, mockElectrum, rates, settings);
|
||||||
};
|
};
|
||||||
|
|||||||
536
tests/cli/mocks/xo-storage-in-memory.ts
Normal file
536
tests/cli/mocks/xo-storage-in-memory.ts
Normal file
@@ -0,0 +1,536 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user