syntax = "proto3"; package spark_token; import "google/protobuf/timestamp.proto"; import "multisig.proto"; import "spark.proto"; import "validate/validate.proto"; option go_package = "github.com/lightsparkdev/spark/proto/spark_token"; service SparkTokenService { // Start process to create final token transaction with all inputs required // from user and SOs (including revocation secret commitment) rpc start_transaction(StartTransactionRequest) returns (StartTransactionResponse) {} // Complete the transaction and commit it with all SOs. This will be // coordinated by one SO. rpc commit_transaction(CommitTransactionRequest) returns (CommitTransactionResponse) {} rpc query_token_metadata(QueryTokenMetadataRequest) returns (QueryTokenMetadataResponse) {} rpc query_token_transactions(QueryTokenTransactionsRequest) returns (QueryTokenTransactionsResponse) {} rpc query_token_outputs(QueryTokenOutputsRequest) returns (QueryTokenOutputsResponse) {} rpc freeze_tokens(FreezeTokensRequest) returns (FreezeTokensResponse) {} // Replaces start_transaction and commit_transaction in single phase transaction flow. rpc broadcast_transaction(BroadcastTransactionRequest) returns (BroadcastTransactionResponse) {} } // This proto is constructed by the wallet to specify leaves it wants to spend // as part of the token transaction. message TokenOutputToSpend { bytes prev_token_transaction_hash = 1 [(validate.rules).bytes.len = 32]; uint32 prev_token_transaction_vout = 2; } message TokenTransferInput { repeated TokenOutputToSpend outputs_to_spend = 1; } message TokenMintInput { // Deprecated: The SO now uses token_identifier to look up the issuer public key // from the TokenCreate record. This field is still included in the transaction hash, // so it must be set correctly, but will be removed in a future version bump. bytes issuer_public_key = 1 [(validate.rules).bytes.len = 33]; optional bytes token_identifier = 2 [(validate.rules).bytes.len = 32]; } message TokenCreateInput { bytes issuer_public_key = 1 [(validate.rules).bytes.len = 33]; string token_name = 2 [(validate.rules).string = {max_len: 20}]; // No minimum length because a single utf-8 character can be 3 bytes. string token_ticker = 3 [(validate.rules).string = {max_len: 6}]; // No minimum length because a single utf-8 character can be 3 bytes. uint32 decimals = 4 [(validate.rules).uint32.lte = 255]; bytes max_supply = 5 [(validate.rules).bytes.len = 16]; // Decoded uint128 bool is_freezable = 6; optional bytes creation_entity_public_key = 7 [(validate.rules).bytes.len = 33]; // If any of the fields below are provided, use protohash to generate the token_identifier optional bytes extra_metadata = 8 [(validate.rules).bytes = {max_len: 1024}]; } // This proto is constructed by the wallet to specify outputs it wants to create // as part of a token transaction. Output id and revocation public key should // remain unfilled so that the SE can fill them as part of the // StartTokenTransaction() call. message TokenOutput { optional string id = 1 [(validate.rules).string.uuid = true]; bytes owner_public_key = 2 [(validate.rules).bytes.len = 33]; optional bytes revocation_commitment = 3 [(validate.rules).bytes.len = 33]; optional uint64 withdraw_bond_sats = 4; optional uint64 withdraw_relative_block_locktime = 5; optional bytes token_public_key = 6 [(validate.rules).bytes = {len: 33}]; optional bytes token_identifier = 8 [(validate.rules).bytes.len = 32]; bytes token_amount = 7 [(validate.rules).bytes.len = 16]; // Decoded uint128 // SE Schnorr signature enabling offline L1 withdrawal. // Only present in query responses; empty until SE signing is implemented. optional bytes se_withdrawal_signature = 9; optional TokenOutputStatus status = 10; } enum TokenOutputStatus { TOKEN_OUTPUT_STATUS_UNSPECIFIED = 0; TOKEN_OUTPUT_STATUS_AVAILABLE = 1; TOKEN_OUTPUT_STATUS_PENDING_OUTBOUND = 2; } message PartialTokenOutput { bytes owner_public_key = 1 [(validate.rules).bytes.len = 33]; uint64 withdraw_bond_sats = 2; uint64 withdraw_relative_block_locktime = 3; bytes token_identifier = 4 [(validate.rules).bytes.len = 32]; bytes token_amount = 5 [(validate.rules).bytes.len = 16]; // Decoded uint128 } message FinalTokenOutput { PartialTokenOutput partial_token_output = 1; bytes revocation_commitment = 2 [(validate.rules).bytes.len = 33]; } enum TokenTransactionType { TOKEN_TRANSACTION_TYPE_UNSPECIFIED = 0; TOKEN_TRANSACTION_TYPE_CREATE = 1; TOKEN_TRANSACTION_TYPE_MINT = 2; TOKEN_TRANSACTION_TYPE_TRANSFER = 3; } // This proto is constructed by the wallet and is the core transaction data // structure. This proto is deterministically hashed to generate the // token_transaction_hash that is cooperatively signed by the SO group to // confirm a token transaction. message TokenTransaction { // For mint transactions issuer_public_key will be specified without any // outputs_to_spend. For transfer transactions the token amount in the input // leaves must match the token amount in the output leaves. uint32 version = 1; oneof token_inputs { TokenMintInput mint_input = 2; TokenTransferInput transfer_input = 3; TokenCreateInput create_input = 8; } repeated TokenOutput token_outputs = 4; repeated bytes spark_operator_identity_public_keys = 5 [(validate.rules).repeated .items.bytes.len = 33]; // Server-set expiry time. The server calculates this by adding the client's // requested validity_duration_seconds to the server's current time when // creating the final transaction. google.protobuf.Timestamp expiry_time = 6; spark.Network network = 7 [(validate.rules).enum = { not_in: [ 0 ] }]; // The timestamp of when the client created the transaction. This is used to // determine which transaction should win in a race condition. Earlier // timestamps win over later ones. google.protobuf.Timestamp client_created_timestamp = 9; // The spark invoices this transaction fulfills. repeated InvoiceAttachment invoice_attachments = 10; // Should NOT be set explicitly on V2 transactions (protected with validation). // Provided here to enable cross-conversion between v3 token transaction protos during migration. optional uint64 validity_duration_seconds = 11; // Optional client-specified deadline for transaction execution. // Carried through from PartialTokenTransaction.execute_before during conversion. optional google.protobuf.Timestamp execute_before = 12; } message TokenTransactionMetadata { repeated bytes spark_operator_identity_public_keys = 2 [(validate.rules).repeated .items.bytes.len = 33]; spark.Network network = 3 [(validate.rules).enum = { not_in: [ 0 ] }]; // The timestamp of when the client created the transaction. This is used to // determine which transaction should win in a race condition. Earlier // timestamps win over later ones. google.protobuf.Timestamp client_created_timestamp = 4; // How long the transaction should be valid for, in seconds. // The server will set the actual expiry_time in the final transaction based // on this duration. Must be within [1, 300] seconds. uint64 validity_duration_seconds = 5 [(validate.rules).uint64 = { gte: 1, lte: 300 }]; // The spark invoices this transaction fulfills. repeated InvoiceAttachment invoice_attachments = 6; } message PartialTokenTransaction { uint32 version = 1; TokenTransactionMetadata token_transaction_metadata = 2; oneof token_inputs { TokenMintInput mint_input = 3; TokenTransferInput transfer_input = 4; TokenCreateInput create_input = 5; } repeated PartialTokenOutput partial_token_outputs = 6; // Optional client-specified deadline for transaction execution. // If set, the server must reject the transaction if current time > execute_before. // Must be after client_created_timestamp, within a configurable max window, // and truncated to microsecond precision. optional google.protobuf.Timestamp execute_before = 7; } message FinalTokenTransaction { uint32 version = 1; TokenTransactionMetadata token_transaction_metadata = 2; oneof token_inputs { TokenMintInput mint_input = 3; TokenTransferInput transfer_input = 4; TokenCreateInput create_input = 5; } repeated FinalTokenOutput final_token_outputs = 6; // Optional client-specified deadline for transaction execution. // Included in the final hash to prevent malleability — an attacker cannot // change the deadline without invalidating operator signatures. optional google.protobuf.Timestamp execute_before = 7; } message InvoiceAttachment { string spark_invoice = 1; } message SignatureWithIndex { // Deprecated: use authority_signatures instead. optional bytes signature = 1 [ (validate.rules).bytes.ignore_empty = true, (validate.rules).bytes.min_len = 64, (validate.rules).bytes.max_len = 73 ]; // The index of the TTXO associated with this signature. uint32 input_index = 2; // Supports single-key or multisig signatures. oneof authority_signatures { multisig.KeyedSignature single_signature = 3; multisig.MultisigSignatureSet multisig_signatures = 4; } } // A group of signatures for the input TTXOs binding them to the final token // transaction hash. This bundle of signatures is specific to a given operator. message InputTtxoSignaturesPerOperator { repeated SignatureWithIndex ttxo_signatures = 1; bytes operator_identity_public_key = 2 [(validate.rules).bytes.len = 33]; } // === Start Transaction === message StartTransactionRequest { bytes identity_public_key = 1 [(validate.rules).bytes.len = 33]; TokenTransaction partial_token_transaction = 2; // Filled by signing the partial token transaction hash with the // owner/issuer private key. For mint transactions this will be one // signature for the input issuer_public_key. For transfer transactions this // will be one signature for each input to be sepnt. repeated SignatureWithIndex partial_token_transaction_owner_signatures = 3; // How long the transaction should be valid for, in seconds. // The server will set the actual expiry_time in the final transaction based // on this duration. Must be within [1, 300] seconds. uint64 validity_duration_seconds = 4 [(validate.rules).uint64 = { gte: 1, lte: 300 }]; } message StartTransactionResponse { TokenTransaction final_token_transaction = 1; spark.SigningKeyshare keyshare_info = 2; } message CommitTransactionRequest { TokenTransaction final_token_transaction = 1; bytes final_token_transaction_hash = 2 [(validate.rules).bytes.len = 32]; repeated InputTtxoSignaturesPerOperator input_ttxo_signatures_per_operator = 3; bytes owner_identity_public_key = 4 [(validate.rules).bytes.len = 33]; } enum CommitStatus { COMMIT_UNSPECIFIED = 0; COMMIT_PROCESSING = 1; COMMIT_FINALIZED = 2; } message CommitProgress { repeated bytes committed_operator_public_keys = 1 [(validate.rules).repeated .items.bytes.len = 33]; repeated bytes uncommitted_operator_public_keys = 2 [(validate.rules).repeated .items.bytes.len = 33]; } message CommitTransactionResponse { CommitStatus commit_status = 1; CommitProgress commit_progress = 2; // The raw token identifier is returned on create transactions optional bytes token_identifier = 3 [(validate.rules).bytes.len = 32]; } message BroadcastTransactionRequest { bytes identity_public_key = 1 [(validate.rules).bytes.len = 33]; PartialTokenTransaction partial_token_transaction = 2; // Filled by signing the partial token transaction hash with the // owner/issuer private key. For mint transactions this will be one // signature for the input issuer_public_key. For transfer transactions this // will be one signature for each input to be spent. repeated SignatureWithIndex token_transaction_owner_signatures = 3; } message BroadcastTransactionResponse { FinalTokenTransaction final_token_transaction = 1; CommitStatus commit_status = 2; CommitProgress commit_progress = 3; // The raw token identifier is returned on create transactions optional bytes token_identifier = 4 [(validate.rules).bytes.len = 32]; } message QueryTokenMetadataRequest { repeated bytes token_identifiers = 1 [(validate.rules).repeated .items.bytes.len = 32]; repeated bytes issuer_public_keys = 2 [(validate.rules).repeated .items.bytes.len = 33]; } message TokenMetadata { bytes issuer_public_key = 1 [(validate.rules).bytes.len = 33]; string token_name = 2 [(validate.rules).string = {max_len: 20}]; string token_ticker = 3 [(validate.rules).string = {max_len: 6}]; uint32 decimals = 4 [(validate.rules).uint32.lte = 255]; bytes max_supply = 5 [(validate.rules).bytes.len = 16]; // Decoded uint128 bool is_freezable = 6; optional bytes creation_entity_public_key = 7 [(validate.rules).bytes.len = 33]; bytes token_identifier = 8 [(validate.rules).bytes.len = 32]; optional bytes extra_metadata = 9 [(validate.rules).bytes = {max_len: 1024}]; } message QueryTokenMetadataResponse { repeated TokenMetadata token_metadata = 1; } message QueryTokenOutputsRequest { repeated bytes owner_public_keys = 1 [(validate.rules).repeated.items.bytes.len = 33]; // Optionally provide issuer public keys or token identifiers. If both are not set return outputs for all tokens. repeated bytes issuer_public_keys = 2 [(validate.rules).repeated.items.bytes.len = 33]; repeated bytes token_identifiers = 4 [(validate.rules).repeated.items.bytes.len = 32]; spark.Network network = 3; // For pagination spark.PageRequest page_request = 5; } // Request constraints are combined using an AND relation. message QueryTokenTransactionsRequest { oneof query_type { QueryTokenTransactionsByTxHash by_tx_hash = 9; QueryTokenTransactionsByFilters by_filters = 10; } // Returns transactions that have one of these output ids in the input or output. repeated string output_ids = 1 [(validate.rules).repeated.items.string.uuid = true]; // Returns transactions that have this owner public key as the sender or receiver in one or more of the outputs. repeated bytes owner_public_keys = 2 [(validate.rules).repeated.items.bytes.len = 33]; // Returns transactions that are related to this token public key. repeated bytes issuer_public_keys = 3 [(validate.rules).repeated.items.bytes.len = 33]; // Returns transactions that are related to this token identifier. repeated bytes token_identifiers = 7 [(validate.rules).repeated.items.bytes.len = 32]; // Returns transactions that match the provided transaction hashes. repeated bytes token_transaction_hashes = 4 [(validate.rules).repeated.items.bytes.len = 32]; spark.Order order = 8; int64 limit = 5 [(validate.rules).int64 = { gte: 0, lte: 1000 }]; int64 offset = 6 [(validate.rules).int64.gte = 0]; } message QueryTokenTransactionsByTxHash { repeated bytes token_transaction_hashes = 1 [(validate.rules).repeated = {min_items: 1, items: {bytes: {len: 32}}}]; } message QueryTokenTransactionsByFilters { // Returns transactions that have one of these output ids in the input or output. repeated string output_ids = 1 [(validate.rules).repeated.items.string.uuid = true]; // Returns transactions that have this owner public key as the sender or receiver in one or more of the output leaves. repeated bytes owner_public_keys = 2 [(validate.rules).repeated.items.bytes.len = 33]; // Returns transactions that are related to this token public key. repeated bytes issuer_public_keys = 3 [(validate.rules).repeated.items.bytes.len = 33]; // Returns transactions that are related to this token identifier. repeated bytes token_identifiers = 4 [(validate.rules).repeated.items.bytes.len = 32]; // For cursor-based pagination spark.PageRequest page_request = 5; } message QueryTokenTransactionsResponse { repeated TokenTransactionWithStatus token_transactions_with_status = 1; int64 offset = 2; spark.PageResponse page_response = 3; } message OutputWithPreviousTransactionData { TokenOutput output = 1; bytes previous_transaction_hash = 2 [(validate.rules).bytes.len = 32]; uint32 previous_transaction_vout = 3; } message QueryTokenOutputsResponse { repeated OutputWithPreviousTransactionData outputs_with_previous_transaction_data = 1; spark.PageResponse page_response = 2; } enum TokenTransactionStatus { TOKEN_TRANSACTION_STARTED = 0; TOKEN_TRANSACTION_SIGNED = 1; TOKEN_TRANSACTION_REVEALED = 5; TOKEN_TRANSACTION_FINALIZED = 2; TOKEN_TRANSACTION_STARTED_CANCELLED = 3; TOKEN_TRANSACTION_SIGNED_CANCELLED = 4; TOKEN_TRANSACTION_UNKNOWN = 10; } message SpentTokenOutputMetadata { string output_id = 1; bytes revocation_secret = 2; } message TokenTransactionConfirmationMetadata { repeated SpentTokenOutputMetadata spent_token_outputs_metadata = 1; } message TokenTransactionWithStatus { TokenTransaction token_transaction = 1; TokenTransactionStatus status = 2; TokenTransactionConfirmationMetadata confirmation_metadata = 3; // In rare cases the above reconstructed token transaction may not match the original token transaction due to: // a) a pre-empted transfer transaction having its input TTXOs remapped to the newer transaction // b) proto migrations or field deprecations resulting in missing/swapped fields (eg. token public key -> token identifier) // Include the original hash to ensure clients can reconcile this transaction with the original if needed. bytes token_transaction_hash = 4 [(validate.rules).bytes.len = 32]; } message FreezeTokensPayload { uint32 version = 1; optional bytes owner_public_key = 2 [(validate.rules).bytes.len = 33]; optional bytes token_public_key = 3 [(validate.rules).bytes.len = 33]; optional bytes token_identifier = 4 [(validate.rules).bytes.len = 32]; uint64 issuer_provided_timestamp = 5; bytes operator_identity_public_key = 6 [(validate.rules).bytes.len = 33]; // Set to false when requesting a freeze. bool should_unfreeze = 7; } message FreezeTokensRequest { FreezeTokensPayload freeze_tokens_payload = 1; // This is a Schnorr or ECDSA DER signature which can be between 64 and 73 bytes. bytes issuer_signature = 2 [(validate.rules).bytes.min_len = 64, (validate.rules).bytes.max_len = 73]; } // Reference to a token output by its outpoint (transaction hash + vout). message TokenOutputRef { bytes transaction_hash = 1 [(validate.rules).bytes.len = 32]; uint32 vout = 2; } // FreezeProgress tracks the coordinated freeze status across operators. message FreezeProgress { repeated bytes applied_operator_public_keys = 1 [(validate.rules).repeated.items.bytes.len = 33]; } message FreezeTokensResponse { // Deprecated: Use impacted_token_outputs instead. UUIDs are SO-local and differ across SOs. repeated string impacted_output_ids = 1 [deprecated = true, (validate.rules).repeated.items.string.uuid = true]; bytes impacted_token_amount = 2; // Decoded uint128 repeated TokenOutputRef impacted_token_outputs = 3; FreezeProgress freeze_progress = 4; }