From 8e239e922bb62e050f3ac032e5f9c6557e674e7c Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Wed, 6 May 2026 16:46:30 -0400 Subject: [PATCH 1/6] spark phase 2 docs --- .../activities/spark-claim-transfer.mdx | 280 ++++++++++ .../spark-prepare-lightning-receive.mdx | 243 +++++++++ .../activities/spark-prepare-transfer.mdx | 296 ++++++++++ api-reference/activities/spark-sign-frost.mdx | 285 ++++++++++ docs.json | 19 +- networks/spark-flows.mdx | 513 ++++++++++++++++++ networks/spark.mdx | 26 + 7 files changed, 1660 insertions(+), 2 deletions(-) create mode 100644 api-reference/activities/spark-claim-transfer.mdx create mode 100644 api-reference/activities/spark-prepare-lightning-receive.mdx create mode 100644 api-reference/activities/spark-prepare-transfer.mdx create mode 100644 api-reference/activities/spark-sign-frost.mdx create mode 100644 networks/spark-flows.mdx diff --git a/api-reference/activities/spark-claim-transfer.mdx b/api-reference/activities/spark-claim-transfer.mdx new file mode 100644 index 00000000..b5627961 --- /dev/null +++ b/api-reference/activities/spark-claim-transfer.mdx @@ -0,0 +1,280 @@ +--- +title: "Spark claim transfer" +description: "Verify the sender's signature, decrypt the leaf key ciphertext, derive the receiver's new leaf key, and produce encrypted claim packages for Spark Operators — all inside the Turnkey enclave." +--- + +import { Authorizations } from "/snippets/api/authorizations.mdx"; +import { H3Bordered } from "/snippets/h3-bordered.mdx"; +import { NestedParam } from "/snippets/nested-param.mdx"; +import { EndpointPath } from "/snippets/api/endpoint.mdx"; + + + + + + + + + +Enum options: `ACTIVITY_TYPE_SPARK_CLAIM_TRANSFER` + + + + +Timestamp (in milliseconds) of the request, used to verify liveness of user requests. + + + + +Unique identifier for a given Organization. + + + +

The parameters object containing the specific intent data for this activity.

+ + + The Spark address of the receiver's signing identity. The enclave uses the corresponding identity private key to ECIES-decrypt the leaf key ciphertext sent by the sender. + + + +

Wraps the claim descriptor.

+ + +

The claim descriptor specifying the transfer to claim and the leaves to take ownership of.

+ + + The UUID of the transfer to claim. Must match the `transferId` used by the sender in `SPARK_PREPARE_TRANSFER`. + + + The sender's Spark identity public key, hex-encoded (compressed secp256k1). The enclave uses this to verify the sender's per-leaf ECDSA signature before decrypting. + + + The Shamir secret sharing threshold for splitting the claim tweak across Spark Operators. Must match the threshold used by the sender. + + +

The Spark Operators that will receive the receiver's encrypted claim tweak shares. Each operator's share is ECIES-encrypted to its individual encryption public key.

+ + + The Spark Operator identifier (e.g. `"SO1"`). + + + The operator's ECIES encryption public key, hex-encoded. + + +
+ +

The individual leaves being claimed. Each entry provides the ECIES-encrypted current leaf key (stored by the Spark Operators on the sender's behalf) and the sender's per-leaf authorization signature.

+ + + The identifier of the leaf being claimed. + + + The ECIES-encrypted blob containing the current leaf private key, hex-encoded. This blob was produced by the sender's enclave during `SPARK_PREPARE_TRANSFER` and stored by the Spark Operators. Retrieve it by polling the Spark Operators for pending transfers. + + + The sender's compact ECDSA signature over `SHA256(leafId || transferId || ciphertext)`, hex-encoded. The enclave verifies this signature against `senderIdentityPublicKey` before decrypting the ciphertext. + + +
+
+
+
+
+
+
+ + + +Enable to have your activity generate and return App Proofs, enabling verifiability. + + + + +A successful response returns the following fields: + + + The activity object containing type, intent, and result + + +Unique identifier for a given Activity object. + + +Unique identifier for a given Organization. + + +The activity status + + +The activity type + + + The intent of the activity + + + The claimSparkTransferIntent object + + +The Spark address of the receiver's signing identity. + + +The claim package request submitted. + + + + + + + The result of the activity + + + The claimSparkTransferResult object + + + One encrypted package per Spark Operator containing the receiver's Feldman-split claim tweak share, ECIES-encrypted to each operator's encryption public key. Send each package to its corresponding operator to complete the key rotation. + + +The Spark Operator identifier this package is addressed to. + + +The ECIES-encrypted claim tweak share for this operator, hex-encoded. + + + + + + + + +A list of objects representing a particular User's approval or rejection of a Consensus request, including all relevant metadata. + + +An artifact verifying a User's action. + + +Whether the activity can be approved. + + +Whether the activity can be rejected. + + +The creation timestamp. + + +The last update timestamp. + + + + + + + +```bash title="cURL" +curl --request POST \ + --url https://api.turnkey.com/public/v1/submit/spark_claim_transfer \ + --header 'Accept: application/json' \ + --header 'Content-Type: application/json' \ + --header "X-Stamp: (see Authorizations)" \ + --data '{ + "type": "ACTIVITY_TYPE_SPARK_CLAIM_TRANSFER", + "timestampMs": " (e.g. 1746736509954)", + "organizationId": " (Your Organization ID)", + "parameters": { + "signWith": "", + "packageRequest": { + "claim": { + "transferId": "", + "senderIdentityPublicKey": "", + "threshold": 2, + "operatorRecipients": [ + { "operatorId": "SO1", "encryptionPublicKey": "" }, + { "operatorId": "SO2", "encryptionPublicKey": "" } + ], + "leaves": [ + { + "leafId": "", + "ciphertext": "", + "senderSignature": "" + } + ] + } + } + } +}' +``` + +```javascript title="JavaScript" +import { Turnkey } from "@turnkey/sdk-server"; + +const turnkeyClient = new Turnkey({ + apiBaseUrl: "https://api.turnkey.com", + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + defaultOrganizationId: process.env.ORGANIZATION_ID!, +}); + +const response = await turnkeyClient.apiClient().sparkClaimTransfer({ + signWith: "", + packageRequest: { + claim: { + transferId: "", + senderIdentityPublicKey: "", + threshold: 2, + operatorRecipients: [ + { operatorId: "SO1", encryptionPublicKey: "" }, + { operatorId: "SO2", encryptionPublicKey: "" }, + ], + leaves: [{ + leafId: "", + ciphertext: "", + senderSignature: "", + }], + }, + }, +}); +``` + + + + + +```json 200 +{ + "activity": { + "id": "", + "status": "ACTIVITY_STATUS_COMPLETED", + "type": "ACTIVITY_TYPE_SPARK_CLAIM_TRANSFER", + "organizationId": "", + "timestampMs": " (e.g. 1746736509954)", + "result": { + "activity": { + "id": "", + "organizationId": "", + "status": "", + "type": "", + "intent": { + "claimSparkTransferIntent": { + "signWith": "", + "packageRequest": "" + } + }, + "result": { + "claimSparkTransferResult": { + "operatorPackages": [ + { "operatorId": "SO1", "encryptedPackage": "" }, + { "operatorId": "SO2", "encryptedPackage": "" } + ] + } + }, + "votes": "", + "fingerprint": "", + "canApprove": "", + "canReject": "", + "createdAt": "", + "updatedAt": "" + } + } + } +} +``` + + diff --git a/api-reference/activities/spark-prepare-lightning-receive.mdx b/api-reference/activities/spark-prepare-lightning-receive.mdx new file mode 100644 index 00000000..5ca6a6a6 --- /dev/null +++ b/api-reference/activities/spark-prepare-lightning-receive.mdx @@ -0,0 +1,243 @@ +--- +title: "Spark prepare Lightning receive" +description: "Generate a random payment preimage inside the Turnkey enclave, Feldman-split it for Spark Operators, and return only the payment hash — the raw preimage never leaves the enclave." +--- + +import { Authorizations } from "/snippets/api/authorizations.mdx"; +import { H3Bordered } from "/snippets/h3-bordered.mdx"; +import { NestedParam } from "/snippets/nested-param.mdx"; +import { EndpointPath } from "/snippets/api/endpoint.mdx"; + + + + + + + + + +Enum options: `ACTIVITY_TYPE_SPARK_PREPARE_LIGHTNING_RECEIVE` + + + + +Timestamp (in milliseconds) of the request, used to verify liveness of user requests. + + + + +Unique identifier for a given Organization. + + + +

The parameters object containing the specific intent data for this activity.

+ + + The Spark address of the receiving identity. Determines the enclave context in which the preimage is generated. + + + +

Configuration for the Lightning receive operation: how many Spark Operators share the preimage and who they are.

+ + + The Shamir secret sharing threshold — the minimum number of Spark Operators that must cooperate to reconstruct the preimage. Must be ≤ the number of `operatorRecipients`. + + +

The Spark Operators that will each hold an encrypted Feldman share of the preimage. When a Lightning payment arrives, threshold operators reconstruct the preimage and reveal it to the Spark Service Provider (SSP) to settle the payment.

+ + + The Spark Operator identifier (e.g. `"SO1"`). + + + The operator's ECIES encryption public key, hex-encoded. The enclave uses this key to encrypt the operator's Feldman share so only that operator can decrypt it. + + +
+
+
+
+
+ + + +Enable to have your activity generate and return App Proofs, enabling verifiability. + + + + +A successful response returns the following fields: + + + The activity object containing type, intent, and result + + +Unique identifier for a given Activity object. + + +Unique identifier for a given Organization. + + +The activity status + + +The activity type + + + The intent of the activity + + + The sparkPrepareLightningReceiveIntent object + + +The Spark address of the receiving identity. + + +The Lightning receive configuration submitted. + + + + + + + The result of the activity + + + The sparkPrepareLightningReceiveResult object + + + One encrypted package per Spark Operator containing that operator's Feldman share of the preimage, ECIES-encrypted to the operator's encryption public key. Submit each package to its corresponding operator so they can reconstruct the preimage when the Lightning payment arrives. + + +The Spark Operator identifier this package is addressed to. + + +The ECIES-encrypted preimage share for this operator, hex-encoded. + + + + +`SHA256(preimage)`, hex-encoded as a 32-byte value. Use this to construct a BOLT11 Lightning invoice. The raw preimage is never returned — it remains inside the enclave and is split across the Spark Operators. + + + + + + +A list of objects representing a particular User's approval or rejection of a Consensus request, including all relevant metadata. + + +An artifact verifying a User's action. + + +Whether the activity can be approved. + + +Whether the activity can be rejected. + + +The creation timestamp. + + +The last update timestamp. + + + + + + + +```bash title="cURL" +curl --request POST \ + --url https://api.turnkey.com/public/v1/submit/spark_prepare_lightning_receive \ + --header 'Accept: application/json' \ + --header 'Content-Type: application/json' \ + --header "X-Stamp: (see Authorizations)" \ + --data '{ + "type": "ACTIVITY_TYPE_SPARK_PREPARE_LIGHTNING_RECEIVE", + "timestampMs": " (e.g. 1746736509954)", + "organizationId": " (Your Organization ID)", + "parameters": { + "signWith": "", + "lightningReceive": { + "threshold": 2, + "operatorRecipients": [ + { "operatorId": "SO1", "encryptionPublicKey": "" }, + { "operatorId": "SO2", "encryptionPublicKey": "" } + ] + } + } +}' +``` + +```javascript title="JavaScript" +import { Turnkey } from "@turnkey/sdk-server"; + +const turnkeyClient = new Turnkey({ + apiBaseUrl: "https://api.turnkey.com", + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + defaultOrganizationId: process.env.ORGANIZATION_ID!, +}); + +const response = await turnkeyClient.apiClient().sparkPrepareLightningReceive({ + signWith: "", + lightningReceive: { + threshold: 2, + operatorRecipients: [ + { operatorId: "SO1", encryptionPublicKey: "" }, + { operatorId: "SO2", encryptionPublicKey: "" }, + ], + }, +}); + +// Use the returned paymentHash to construct a BOLT11 invoice. +// The raw preimage never leaves the enclave. +console.log("Payment hash:", response.paymentHash); +``` + + + + + +```json 200 +{ + "activity": { + "id": "", + "status": "ACTIVITY_STATUS_COMPLETED", + "type": "ACTIVITY_TYPE_SPARK_PREPARE_LIGHTNING_RECEIVE", + "organizationId": "", + "timestampMs": " (e.g. 1746736509954)", + "result": { + "activity": { + "id": "", + "organizationId": "", + "status": "", + "type": "", + "intent": { + "sparkPrepareLightningReceiveIntent": { + "signWith": "", + "lightningReceive": "" + } + }, + "result": { + "sparkPrepareLightningReceiveResult": { + "operatorPackages": [ + { "operatorId": "SO1", "encryptedPackage": "" }, + { "operatorId": "SO2", "encryptedPackage": "" } + ], + "paymentHash": "<32-byte-hex>" + } + }, + "votes": "", + "fingerprint": "", + "canApprove": "", + "canReject": "", + "createdAt": "", + "updatedAt": "" + } + } + } +} +``` + + diff --git a/api-reference/activities/spark-prepare-transfer.mdx b/api-reference/activities/spark-prepare-transfer.mdx new file mode 100644 index 00000000..ce427ce0 --- /dev/null +++ b/api-reference/activities/spark-prepare-transfer.mdx @@ -0,0 +1,296 @@ +--- +title: "Spark prepare transfer" +description: "Atomically compute key tweaks, Feldman-split them for Spark Operators, encrypt the receiver's new leaf key, and sign the transfer payload — all inside the Turnkey enclave." +--- + +import { Authorizations } from "/snippets/api/authorizations.mdx"; +import { H3Bordered } from "/snippets/h3-bordered.mdx"; +import { NestedParam } from "/snippets/nested-param.mdx"; +import { EndpointPath } from "/snippets/api/endpoint.mdx"; + + + + + + + + + +Enum options: `ACTIVITY_TYPE_SPARK_PREPARE_TRANSFER` + + + + +Timestamp (in milliseconds) of the request, used to verify liveness of user requests. + + + + +Unique identifier for a given Organization. + + + +

The parameters object containing the specific intent data for this activity.

+ + + The Spark address of the sender's signing identity. Determines which enclave-held identity key is used to derive leaf keys and sign the transfer payload. + + + +

The complete transfer descriptor including the receiver, Spark Operators, and all leaves being transferred.

+ + + A UUID v7 that uniquely identifies this transfer. Used by the enclave and Spark Operators to correlate all messages in the transfer flow. + + + The Shamir secret sharing threshold — the minimum number of Spark Operators that must cooperate to reconstruct any key tweak share. Must be ≤ the number of `operatorRecipients`. + + + The receiver's Spark identity public key, hex-encoded (compressed secp256k1). The enclave ECIES-encrypts the receiver's new leaf private key to this public key so only the receiver can decrypt it during the claim step. + + +

The list of Spark Operators that will receive encrypted key tweak shares. Each operator's Shamir share is ECIES-encrypted to its individual encryption public key.

+ + + The Spark Operator identifier (e.g. `"SO1"`). + + + The operator's ECIES encryption public key, hex-encoded. The enclave uses this key to encrypt the operator's Shamir share so only that operator can decrypt it. + + +
+ +

The list of Spark leaves being transferred. Each entry specifies the old leaf key, the new leaf key, and the pre-aggregated refund signatures that were produced in the preceding `SPARK_SIGN_FROST` call.

+ + + The identifier of the leaf being transferred. + + + Derivation descriptor for the sender's current leaf key. Must use `type: SPARK_KEY_TYPE_SIGNING_HD` with the current leaf's `leafId`. The enclave derives the current leaf private key from this path to compute the tweak. + + + Derivation descriptor for the receiver's new leaf key. Must use `type: SPARK_KEY_TYPE_SIGNING_HD` with the new leaf's `leafId`. The enclave derives this key and encrypts it to the receiver's identity public key. + + + The fully aggregated Schnorr signature for the new leaf's CPFP refund transaction, hex-encoded. Produced by aggregating the partial signatures from `SPARK_SIGN_FROST` with the Spark Operators' shares. + + + The fully aggregated Schnorr signature for the new leaf's direct refund transaction, hex-encoded. + + + The fully aggregated Schnorr signature for the new leaf's direct-from-CPFP refund transaction, hex-encoded. + + +
+
+
+
+
+ + + +Enable to have your activity generate and return App Proofs, enabling verifiability. + + + + +A successful response returns the following fields: + + + The activity object containing type, intent, and result + + +Unique identifier for a given Activity object. + + +Unique identifier for a given Organization. + + +The activity status + + +The activity type + + + The intent of the activity + + + The prepareSparkTransferIntent object + + +The Spark address of the sender's signing identity. + + +The transfer descriptor submitted. + + + + + + + The result of the activity + + + The prepareSparkTransferResult object + + + One encrypted package per Spark Operator. Each package contains that operator's Feldman-split key tweak share, ECIES-encrypted to the operator's encryption public key. Send each package to its corresponding operator. + + +The Spark Operator identifier this package is addressed to. + + +The ECIES-encrypted key tweak share for this operator, hex-encoded. + + + + +The sender's ECDSA signature over the transfer payload (DER-encoded, hex), produced with the sender's Spark identity key inside the enclave. Spark Operators use this signature to verify that the transfer was authorized by the legitimate key holder. + + + + + + +A list of objects representing a particular User's approval or rejection of a Consensus request, including all relevant metadata. + + +An artifact verifying a User's action. + + +Whether the activity can be approved. + + +Whether the activity can be rejected. + + +The creation timestamp. + + +The last update timestamp. + + + + + + + +```bash title="cURL" +curl --request POST \ + --url https://api.turnkey.com/public/v1/submit/spark_prepare_transfer \ + --header 'Accept: application/json' \ + --header 'Content-Type: application/json' \ + --header "X-Stamp: (see Authorizations)" \ + --data '{ + "type": "ACTIVITY_TYPE_SPARK_PREPARE_TRANSFER", + "timestampMs": " (e.g. 1746736509954)", + "organizationId": " (Your Organization ID)", + "parameters": { + "signWith": "", + "transfer": { + "transferId": "", + "threshold": 2, + "receiverPublicKey": "", + "operatorRecipients": [ + { "operatorId": "SO1", "encryptionPublicKey": "" }, + { "operatorId": "SO2", "encryptionPublicKey": "" } + ], + "leaves": [ + { + "leafId": "", + "oldLeafDerivation": { + "type": "SPARK_KEY_TYPE_SIGNING_HD", + "leafId": "" + }, + "newLeafDerivation": { + "type": "SPARK_KEY_TYPE_SIGNING_HD", + "leafId": "" + }, + "refundSignature": "", + "directRefundSignature": "", + "directFromCpfpRefundSignature": "" + } + ] + } + } +}' +``` + +```javascript title="JavaScript" +import { Turnkey } from "@turnkey/sdk-server"; + +const turnkeyClient = new Turnkey({ + apiBaseUrl: "https://api.turnkey.com", + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + defaultOrganizationId: process.env.ORGANIZATION_ID!, +}); + +const response = await turnkeyClient.apiClient().sparkPrepareTransfer({ + signWith: "", + transfer: { + transferId: "", + threshold: 2, + receiverPublicKey: "", + operatorRecipients: [ + { operatorId: "SO1", encryptionPublicKey: "" }, + { operatorId: "SO2", encryptionPublicKey: "" }, + ], + leaves: [{ + leafId: "", + oldLeafDerivation: { type: "SPARK_KEY_TYPE_SIGNING_HD", leafId: "" }, + newLeafDerivation: { type: "SPARK_KEY_TYPE_SIGNING_HD", leafId: "" }, + refundSignature: "", + directRefundSignature: "", + directFromCpfpRefundSignature: "", + }], + }, +}); +``` + + + + + +```json 200 +{ + "activity": { + "id": "", + "status": "ACTIVITY_STATUS_COMPLETED", + "type": "ACTIVITY_TYPE_SPARK_PREPARE_TRANSFER", + "organizationId": "", + "timestampMs": " (e.g. 1746736509954)", + "result": { + "activity": { + "id": "", + "organizationId": "", + "status": "", + "type": "", + "intent": { + "prepareSparkTransferIntent": { + "signWith": "", + "transfer": "" + } + }, + "result": { + "prepareSparkTransferResult": { + "operatorPackages": [ + { "operatorId": "SO1", "encryptedPackage": "" }, + { "operatorId": "SO2", "encryptedPackage": "" } + ], + "transferUserSignature": "" + } + }, + "votes": "", + "fingerprint": "", + "canApprove": "", + "canReject": "", + "createdAt": "", + "updatedAt": "" + } + } + } +} +``` + + diff --git a/api-reference/activities/spark-sign-frost.mdx b/api-reference/activities/spark-sign-frost.mdx new file mode 100644 index 00000000..defe33bf --- /dev/null +++ b/api-reference/activities/spark-sign-frost.mdx @@ -0,0 +1,285 @@ +--- +title: "Spark FROST sign" +description: "Generate FROST partial signatures for one or more Bitcoin transaction sighashes using a key held in the Turnkey enclave." +--- + +import { Authorizations } from "/snippets/api/authorizations.mdx"; +import { H3Bordered } from "/snippets/h3-bordered.mdx"; +import { NestedParam } from "/snippets/nested-param.mdx"; +import { EndpointPath } from "/snippets/api/endpoint.mdx"; + + + + + + + + + +Enum options: `ACTIVITY_TYPE_SPARK_SIGN_FROST` + + + + +Timestamp (in milliseconds) of the request, used to verify liveness of user requests. + + + + +Unique identifier for a given Organization. + + + +

The parameters object containing the specific intent data for this activity.

+ + + The Spark address of the signing identity (e.g. `spark1pgss...`). Determines which enclave-held identity key is used to derive leaf signing keys. + + + +

A batch of one or more signature requests. A single call can sign multiple transactions (e.g. branch + exit at deposit time, or up to 6 refund transactions per transfer).

+ + + The key derivation descriptor that identifies which leaf key to use for this signature. + + + Selects the key derivation strategy. + + | Value | Required Field | Used For | + | --- | --- | --- | + | `SPARK_KEY_TYPE_DEPOSIT` | _(none)_ | Single-use deposit transactions | + | `SPARK_KEY_TYPE_STATIC_DEPOSIT_HD` | `index` | Static (reusable) deposit transactions | + | `SPARK_KEY_TYPE_SIGNING_HD` | `leafId` | Active leaf signing — withdrawals, transfer refunds | + + Enum options: `SPARK_KEY_TYPE_DEPOSIT`, `SPARK_KEY_TYPE_STATIC_DEPOSIT_HD`, `SPARK_KEY_TYPE_SIGNING_HD` + + + Required when `type` is `SPARK_KEY_TYPE_SIGNING_HD`. Identifies the specific Spark leaf whose HD-derived signing key should be used. + + + Required when `type` is `SPARK_KEY_TYPE_STATIC_DEPOSIT_HD`. The integer index of the static deposit address (e.g. `0` for the first static deposit address at path `m/8797555'/{account}'/3'/0'`). + + + + + The Bitcoin transaction sighash to sign, encoded as a hex string. Must match the sighash committed to in `operatorCommitments`. + + + The aggregate Taproot public key (user key + Spark Operator key shares combined), hex-encoded. Used by the enclave to validate that the partial signature is consistent with the expected aggregate key. + + +

The FROST nonce commitments provided by each participating Spark Operator for this signature session. Each operator must provide both a hiding and binding commitment before signing begins.

+ + + The Spark Operator identifier (e.g. `"SO1"`). + + + The operator's FROST hiding nonce commitment, hex-encoded. + + + The operator's FROST binding nonce commitment, hex-encoded. + + +
+
+
+
+
+ + + +Enable to have your activity generate and return App Proofs, enabling verifiability. + + + + +A successful response returns the following fields: + + + The activity object containing type, intent, and result + + +Unique identifier for a given Activity object. + + +Unique identifier for a given Organization. + + +The activity status + + +The activity type + + + The intent of the activity + + + The sparkSignFrostIntent object + + +The Spark address of the signing identity. + + +The batch of signature requests submitted. + + + + + + + The result of the activity + + + The sparkSignFrostResult object + + + One entry per input signature request, in the same order as the request array. + + +The Turnkey enclave's FROST partial signature share for this sighash, hex-encoded. The client must aggregate this with the Spark Operators' partial signature shares to produce the final Schnorr signature. + + +The hiding nonce generated inside the enclave for this signing session, hex-encoded. Must be forwarded to the Spark Operators so they can complete FROST aggregation. + + +The binding nonce generated inside the enclave for this signing session, hex-encoded. Must be forwarded to the Spark Operators so they can complete FROST aggregation. + + + + + + + + +A list of objects representing a particular User's approval or rejection of a Consensus request, including all relevant metadata. + + +An artifact verifying a User's action. + + +Whether the activity can be approved. + + +Whether the activity can be rejected. + + +The creation timestamp. + + +The last update timestamp. + + + + + + + +```bash title="cURL" +curl --request POST \ + --url https://api.turnkey.com/public/v1/submit/spark_sign_frost \ + --header 'Accept: application/json' \ + --header 'Content-Type: application/json' \ + --header "X-Stamp: (see Authorizations)" \ + --data '{ + "type": "ACTIVITY_TYPE_SPARK_SIGN_FROST", + "timestampMs": " (e.g. 1746736509954)", + "organizationId": " (Your Organization ID)", + "parameters": { + "signWith": "", + "signatures": [ + { + "derivation": { + "type": "SPARK_KEY_TYPE_SIGNING_HD", + "leafId": "" + }, + "message": "", + "verifyingKey": "", + "operatorCommitments": [ + { + "id": "SO1", + "hiding": "", + "binding": "" + } + ] + } + ] + } +}' +``` + +```javascript title="JavaScript" +import { Turnkey } from "@turnkey/sdk-server"; + +const turnkeyClient = new Turnkey({ + apiBaseUrl: "https://api.turnkey.com", + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + defaultOrganizationId: process.env.ORGANIZATION_ID!, +}); + +const response = await turnkeyClient.apiClient().sparkSignFrost({ + signWith: "", + signatures: [{ + derivation: { + type: "SPARK_KEY_TYPE_SIGNING_HD", + leafId: "", + }, + message: "", + verifyingKey: "", + operatorCommitments: [{ + id: "SO1", + hiding: "", + binding: "", + }], + }], +}); +``` + + + + + +```json 200 +{ + "activity": { + "id": "", + "status": "ACTIVITY_STATUS_COMPLETED", + "type": "ACTIVITY_TYPE_SPARK_SIGN_FROST", + "organizationId": "", + "timestampMs": " (e.g. 1746736509954)", + "result": { + "activity": { + "id": "", + "organizationId": "", + "status": "", + "type": "", + "intent": { + "sparkSignFrostIntent": { + "signWith": "", + "signatures": "" + } + }, + "result": { + "sparkSignFrostResult": { + "signatures": [ + { + "signatureShare": "", + "hiding": "", + "binding": "" + } + ] + } + }, + "votes": "", + "fingerprint": "", + "canApprove": "", + "canReject": "", + "createdAt": "", + "updatedAt": "" + } + } + } +} +``` + + diff --git a/docs.json b/docs.json index cd180b90..22b5f6dd 100644 --- a/docs.json +++ b/docs.json @@ -8,7 +8,12 @@ "dark": "#050a0b" }, "contextual": { - "options": ["copy", "view", "chatgpt", "claude"] + "options": [ + "copy", + "view", + "chatgpt", + "claude" + ] }, "integrations": { "ga4": { @@ -279,7 +284,13 @@ ] }, "networks/bitcoin", - "networks/spark", + { + "group": "Spark", + "pages": [ + "networks/spark", + "networks/spark-flows" + ] + }, "networks/hyperliquid", "networks/cosmos", "networks/tron", @@ -830,6 +841,10 @@ "api-reference/activities/sign-raw-payload", "api-reference/activities/sign-raw-payloads", "api-reference/activities/sign-transaction", + "api-reference/activities/spark-claim-transfer", + "api-reference/activities/spark-prepare-lightning-receive", + "api-reference/activities/spark-prepare-transfer", + "api-reference/activities/spark-sign-frost", "api-reference/activities/update-a-fiat-on-ramp-credential", "api-reference/activities/update-an-oauth-20-credential", "api-reference/activities/update-organization-name", diff --git a/networks/spark-flows.mdx b/networks/spark-flows.mdx new file mode 100644 index 00000000..e9c6be9c --- /dev/null +++ b/networks/spark-flows.mdx @@ -0,0 +1,513 @@ +--- +title: "Spark flows" +sidebarTitle: "Flows" +description: "Step-by-step reference for every Turnkey-integrated Spark operation: deposits, withdrawals, transfers, Lightning receives, and exits." +--- + +This page documents each Spark flow in detail, showing exactly which Turnkey API activities are called at each step and what happens inside the enclave. All private key material and secret shares remain inside the Turnkey enclave throughout — they never reach client code. + +For quick-reference code examples, see the [Spark network overview](/networks/spark). + +--- + +## 1. Deposit: Bitcoin L1 → Spark + +**Turnkey activities:** `CREATE_WALLET_ACCOUNTS` ×1, `SPARK_SIGN_FROST` ×1 (batch of 2) + +### Step 1 — Derive deposit key + +Call [`CREATE_WALLET_ACCOUNTS`](/api-reference/activities/create-wallet-accounts) to derive and register a single-use deposit key in the enclave: + +```json +{ + "walletId": "", + "accounts": [{ + "curve": "CURVE_SECP256K1", + "pathFormat": "PATH_FORMAT_BIP32", + "path": "m/8797555'/0'/2'", + "addressFormat": "ADDRESS_FORMAT_COMPRESSED" + }] +} +``` + +The response contains the compressed deposit public key. This account now exists in Turnkey and can be referenced in future signing calls. + +### Step 2 — Get SO public key shares + +Call the Spark Operators (SOs): "I want to start a deposit." The SOs return their key share for this leaf. + +### Step 3 — Compute aggregate public key and Taproot deposit address + +Client-side: `P_aggregate = P_user_deposit + P_so`. Derive the `bc1p...` Taproot address. + +### Step 4 — Construct branch and exit transactions + +Client-side: build unsigned `Txn1` (branch) and `Txn2` (exit, timelocked) using the aggregate public key. + +### Step 5 — Get SO signing commitments + +Call the SOs: "I need to sign a branch transaction and an exit transaction." The SOs return two sets of `operatorCommitments[]`. + +### Step 6 — FROST sign branch and exit transactions + +Call [`SPARK_SIGN_FROST`](/api-reference/activities/spark-sign-frost) with a batch of 2 signatures: + +```json +{ + "signWith": "spark1pgss...", + "signatures": [ + { + "derivation": { "type": "SPARK_KEY_TYPE_DEPOSIT" }, + "message": "", + "operatorCommitments": [{ "id": "SO1", "hiding": "", "binding": "" }], + "verifyingKey": "" + }, + { + "derivation": { "type": "SPARK_KEY_TYPE_DEPOSIT" }, + "message": "", + "operatorCommitments": [{ "id": "SO1", "hiding": "", "binding": "" }], + "verifyingKey": "" + } + ] +} +``` + +The enclave generates fresh nonces and returns 2 `(signatureShare, hiding, binding)` tuples. The client aggregates each with the SOs' partial signatures to produce 2 final Schnorr signatures. Both transactions are now fully signed. + +### Step 7 — Store signed exit transactions + +Store `Txn1` and `Txn2` securely. These are the permanent unilateral exit path — see [Unilateral Exit](#3-unilateral-exit-spark--bitcoin-l1-emergency). + +### Step 8 — Broadcast deposit transaction + +Broadcast a standard Bitcoin transaction sending BTC to the `bc1p...` Taproot address. + +### Step 9 — Wait for L1 confirmation + +The SOs register the new Spark leaf once the transaction confirms. + +--- + +## 2. Cooperative Withdrawal: Spark → Bitcoin L1 + +**Turnkey activities:** `SPARK_SIGN_FROST` ×1 + +### Step 1 — Construct withdrawal transaction + +Client-side: build an unsigned Bitcoin L1 transaction spending from the leaf's Taproot address to the user's personal Bitcoin address. + +### Step 2 — Get SO signing commitments + +Call the SOs: "I want to cooperatively withdraw leaf X." The SOs return `operatorCommitments[]`. + +### Step 3 — FROST sign withdrawal transaction + +Call [`SPARK_SIGN_FROST`](/api-reference/activities/spark-sign-frost): + +```json +{ + "signWith": "spark1pgss...", + "signatures": [{ + "derivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "leaf-abc-123" }, + "message": "", + "operatorCommitments": [{ "id": "SO1", "hiding": "", "binding": "" }], + "verifyingKey": "" + }] +} +``` + +The enclave returns `(signatureShare, hiding, binding)`. The client forwards the nonces to the SOs and aggregates all partial signatures into the final Schnorr signature. + +### Step 4 — Broadcast withdrawal transaction + +Attach the final signature and broadcast to Bitcoin L1. + +### Step 5 — Wait for confirmation + +BTC arrives at the user's L1 address. The SOs deactivate the leaf. + +--- + +## 3. Unilateral Exit: Spark → Bitcoin L1 (Emergency) + +**Turnkey activities:** None + +This path uses transactions that were pre-signed at deposit time. No Turnkey API calls are required. + +### Step 1 — Retrieve pre-signed exit transactions + +Retrieve the stored `Txn1` (branch) and `Txn2` (exit) from deposit time. + +### Step 2 — Broadcast branch transaction + +Broadcast `Txn1` to Bitcoin L1. Once mined, the relative timelock countdown begins. + +### Step 3 — Wait for timelock + +Wait approximately 100 blocks (~16 hours). + +### Step 4 — Broadcast exit transaction + +Broadcast `Txn2`. BTC arrives at the user's L1 address. + + + The unilateral exit path does not require any Turnkey API calls because the exit transactions were fully signed at deposit time using `SPARK_SIGN_FROST`. + + +--- + +## 4. Initiate Transfer: Spark → Spark (Sender) + +**Turnkey activities:** `SPARK_SIGN_FROST` ×1 (batch of up to 6), `SPARK_PREPARE_TRANSFER` ×1 + +### Step 1 — Select input leaves + +Query the SOs for the sender's active leaves and select which cover the transfer amount. No Turnkey call. + +### Step 2 — Get SO signing commitments for all new exit transactions + +Call the SOs: "I need to sign refund transactions for N new leaves." The SOs return `operatorCommitments[]` for each of 3 refund transaction types per new leaf (CPFP, direct, direct-from-CPFP). For 2 new leaves (recipient leaf + change leaf) this yields 6 sets of commitments. + +### Step 3 — FROST sign all refund transactions + +Call [`SPARK_SIGN_FROST`](/api-reference/activities/spark-sign-frost) with a batch of up to 6 signatures (3 types × up to 2 leaves): + +```json +{ + "signWith": "spark1pgss...", + "signatures": [ + { + "derivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "bob-new-leaf-id" }, + "message": "", + "operatorCommitments": [...], + "verifyingKey": "" + }, + { + "derivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "bob-new-leaf-id" }, + "message": "", + "operatorCommitments": [...], + "verifyingKey": "" + }, + { + "derivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "bob-new-leaf-id" }, + "message": "", + "operatorCommitments": [...], + "verifyingKey": "" + }, + { + "derivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "alice-change-leaf-id" }, + "message": "", + "operatorCommitments": [...], + "verifyingKey": "" + }, + { + "derivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "alice-change-leaf-id" }, + "message": "", + "operatorCommitments": [...], + "verifyingKey": "" + }, + { + "derivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "alice-change-leaf-id" }, + "message": "", + "operatorCommitments": [...], + "verifyingKey": "" + } + ] +} +``` + +The enclave returns 6 `(signatureShare, hiding, binding)` tuples. The client aggregates each with the SOs' partial signatures to produce 6 final Schnorr signatures. All new leaf exit transactions are now pre-signed. + +### Step 4 — Build the transfer package + +Call [`SPARK_PREPARE_TRANSFER`](/api-reference/activities/spark-prepare-transfer) with the aggregated refund signatures from Step 3: + +```json +{ + "signWith": "spark1pgss...", + "transfer": { + "transferId": "aaaaa-bbbb-...", + "threshold": 2, + "receiverPublicKey": "", + "operatorRecipients": [ + { "operatorId": "SO1", "encryptionPublicKey": "02abc..." }, + { "operatorId": "SO2", "encryptionPublicKey": "02def..." } + ], + "leaves": [ + { + "leafId": "old-leaf-id", + "oldLeafDerivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "old-leaf-id" }, + "newLeafDerivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "bob-new-leaf-id" }, + "refundSignature": "", + "directRefundSignature": "", + "directFromCpfpRefundSignature": "" + }, + { + "leafId": "old-leaf-id", + "oldLeafDerivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "old-leaf-id" }, + "newLeafDerivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "alice-change-leaf-id" }, + "refundSignature": "", + "directRefundSignature": "", + "directFromCpfpRefundSignature": "" + } + ] + } +} +``` + +The enclave atomically: +1. Derives old and new leaf keys from the identity key's HD wallet +2. Computes `tweak = oldKey − newKey` per leaf +3. Feldman-splits each tweak for the SOs +4. ECIES-encrypts Bob's new leaf key to his identity public key +5. ECIES-encrypts each SO's Shamir share to their encryption public key +6. ECDSA-signs the per-leaf payload and the outer transfer payload with the identity key + +Returns `operatorPackages[]` and `transferUserSignature`. + +### Step 5 — Submit to Spark Operators + +Send to the SOs: +- The aggregated refund signatures from Step 3 +- The encrypted operator packages from Step 4 +- The `transferUserSignature` from Step 4 + +The SOs verify, rotate key shares, delete old shares, store the ECIES blob for Bob, register new leaves, and activate watchtowers. + +--- + +## 5. Claim Transfer: Spark → Spark (Receiver) + +**Turnkey activities:** `SPARK_CLAIM_TRANSFER` ×1 + +### Step 1 — Poll SOs for pending transfers + +Call the SOs: "Any pending transfers for me?" The SOs return the encrypted blob, leaf ID, transfer ID, and sender identity public key. No Turnkey call. + +### Step 2 — Claim the transfer + +Call [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/spark-claim-transfer): + +```json +{ + "signWith": "spark1pbob...", + "packageRequest": { + "claim": { + "transferId": "aaaaa-bbbb-...", + "senderIdentityPublicKey": "", + "threshold": 2, + "operatorRecipients": [ + { "operatorId": "SO1", "encryptionPublicKey": "02abc..." }, + { "operatorId": "SO2", "encryptionPublicKey": "02def..." } + ], + "leaves": [{ + "leafId": "bob-new-leaf-id", + "ciphertext": "", + "senderSignature": "" + }] + } + } +} +``` + +The enclave atomically: +1. Verifies Alice's ECDSA signature over `SHA256(leafId || transferId || ciphertext)` +2. ECIES-decrypts the ciphertext using Bob's identity private key to recover `current_leaf_priv` +3. Derives Bob's new leaf key from his HD wallet +4. Computes `claim_tweak = current_leaf_priv − new_leaf_priv` +5. Feldman-splits the claim tweak for the SOs +6. ECIES-encrypts each SO's claim share to their encryption public key + +Returns `operatorPackages[]`. + +### Step 3 — Submit claim packages to SOs + +Send the encrypted packages to the SOs. Each SO applies the tweak, rotating the leaf key to Bob's HD-derived key. + +### Step 4 — Store new leaf in local wallet state + +Record the `leafId → derivation path` mapping locally. Bob's wallet now tracks this leaf. + +--- + +## 6. Lightning Receive + +**Turnkey activities:** `SPARK_PREPARE_LIGHTNING_RECEIVE` ×1 + +### Step 1 — Generate preimage and distribute shares to SOs + +Call [`SPARK_PREPARE_LIGHTNING_RECEIVE`](/api-reference/activities/spark-prepare-lightning-receive): + +```json +{ + "signWith": "spark1pgss...", + "lightningReceive": { + "threshold": 2, + "operatorRecipients": [ + { "operatorId": "SO1", "encryptionPublicKey": "02abc..." }, + { "operatorId": "SO2", "encryptionPublicKey": "02def..." } + ] + } +} +``` + +The enclave: +1. Generates a random 32-byte preimage using a secure RNG +2. Computes `paymentHash = SHA256(preimage)` +3. Feldman-splits the preimage across the SOs +4. ECIES-encrypts each SO's share to their encryption public key + +Returns `operatorPackages[]` and `paymentHash`. The raw preimage never leaves the enclave. + +### Step 2 — Submit preimage shares to SOs + +Send the encrypted operator packages to the SOs. Each SO stores its share and will contribute to preimage reconstruction when the Lightning payment arrives. + +### Step 3 — Create Lightning invoice + +Construct a BOLT11 invoice encoding `paymentHash`. Share this invoice with the payer. + +### Step 4 — Lock leaves to the SSP + +Tell the Spark Service Provider (SSP): "I'll transfer X sats of my leaves to you if you pay this invoice." The SOs lock the specified leaves. + +### Step 5 — SSP pays the Lightning invoice + +The SSP routes the payment across the Lightning Network. + +### Step 6 — SOs reveal preimage to SSP + +Threshold SOs reconstruct the preimage from their Feldman shares and give it to the SSP. + +### Step 7 — SSP completes the Lightning payment + +The SSP reveals the preimage to the payer's node. The Lightning payment settles. + +### Step 8 — SOs transfer leaves to the SSP + +The SOs atomically execute the leaf transfer from the receiver to the SSP. + +### Timeout path + +If the SSP doesn't pay within the time limit, the SOs unlock the receiver's leaves. No funds are lost. + +--- + +## 7. Static Deposit: Bitcoin L1 → Spark (Reusable Address) + +**Turnkey activities:** `CREATE_WALLET_ACCOUNTS` ×1 (once per index, reused thereafter), `SPARK_SIGN_FROST` ×1 (batch of 2) + +A static deposit address is deterministic and permanent — the same path at index N always produces the same Taproot address and can receive multiple deposits, each creating a separate Spark leaf. + +### Step 1 — Derive static deposit key + +Call [`CREATE_WALLET_ACCOUNTS`](/api-reference/activities/create-wallet-accounts) once per index. Increment the final path segment for additional static addresses: + +```json +{ + "walletId": "", + "accounts": [{ + "curve": "CURVE_SECP256K1", + "pathFormat": "PATH_FORMAT_BIP32", + "path": "m/8797555'/0'/3'/0'", + "addressFormat": "ADDRESS_FORMAT_COMPRESSED" + }] +} +``` + +This is a deterministic, reusable key — call once and reuse the same address for all future deposits at that index. + +### Step 2 — Get SO public key shares + +Call the SOs: "I want to register a static deposit address at index 0." The SOs return their key share for this index. + +### Step 3 — Compute aggregate public key and Taproot deposit address + +Client-side: `P_aggregate = P_static_deposit_user + P_so`. Derive the permanent `bc1p...` Taproot address for this index. + +### Step 4 — Construct branch and exit transactions + +Client-side: build unsigned `Txn1` (branch) and `Txn2` (exit, timelocked) using the aggregate public key. + +### Step 5 — Get SO signing commitments + +Call the SOs: "I need to sign branch and exit transactions for static deposit index 0." The SOs return two sets of `operatorCommitments[]`. + +### Step 6 — FROST sign branch and exit transactions + +Call [`SPARK_SIGN_FROST`](/api-reference/activities/spark-sign-frost) using `SPARK_KEY_TYPE_STATIC_DEPOSIT_HD` and the index: + +```json +{ + "signWith": "spark1pgss...", + "signatures": [ + { + "derivation": { "type": "SPARK_KEY_TYPE_STATIC_DEPOSIT_HD", "index": 0 }, + "message": "", + "operatorCommitments": [...], + "verifyingKey": "" + }, + { + "derivation": { "type": "SPARK_KEY_TYPE_STATIC_DEPOSIT_HD", "index": 0 }, + "message": "", + "operatorCommitments": [...], + "verifyingKey": "" + } + ] +} +``` + +Returns 2 `(signatureShare, hiding, binding)` tuples. The client aggregates with SO partial signatures to produce 2 final Schnorr signatures. + +### Step 7 — Store signed exit transactions + +Store `Txn1` and `Txn2`. Each individual deposit to this address generates a separate leaf with its own exit transactions. + +### Step 8 — Broadcast deposit transaction + +Broadcast a standard Bitcoin transaction sending BTC to the static `bc1p...` Taproot address. + +### Step 9 — Wait for L1 confirmation + +The SOs register a new Spark leaf for each confirmed deposit to this address. + +--- + +## 8. Static Deposit Claim + +**Turnkey activities:** `EXPORT_WALLET_ACCOUNT` ×1 + + + The Spark SDK's static deposit claim path requires the raw private key to sign the claim transaction directly. Turnkey exports the key into a P256-encrypted bundle that only your local session can decrypt. The key is held in memory only for the duration of the claim. + + +### Step 1 — Confirmed deposit detected + +After a deposit to the static address confirms on-chain, the SOs register a new leaf. Fetch an SSP quote: + +```typescript +const quote = await wallet.getClaimStaticDepositQuote(txid, outputIndex); +``` + +### Step 2 — Export static deposit key + +Call [`EXPORT_WALLET_ACCOUNT`](/api-reference/activities/export-wallet-account) to retrieve the private key in a P256-encrypted bundle that only the local session can decrypt: + +```json +{ + "address": "", + "targetPublicKey": "" +} +``` + +The client decrypts the export bundle locally using the ephemeral P256 private key. + +### Step 3 — Claim the static deposit + +1. Install the decrypted private key: `signer.setStaticDepositSecretKey(0, key)` +2. Call `wallet.claimStaticDeposit({ transactionId, creditAmountSats, sspSignature })` +3. Immediately zero the key: `signer.clearStaticDepositSecretKey(0)` + +### Step 4 — Wait for balance availability + +Poll `wallet.getBalance()` until the credited sats appear as available. diff --git a/networks/spark.mdx b/networks/spark.mdx index 4c5fad1e..062868d9 100644 --- a/networks/spark.mdx +++ b/networks/spark.mdx @@ -56,6 +56,32 @@ Turnkey automatically selects the correct signing scheme based on the address fo - **Bech32m addressing**: Spark identity key addresses are Bech32m-encoded canonical protobuf payloads - **Spark-specific BIP-32 purpose**: Derivation path uses purpose `8797555` per the Spark protocol spec +## Spark-specific API activities + +In addition to `SIGN_RAW_PAYLOAD`, Turnkey exposes four Spark-specific activities for enclave-protected FROST signing, key transfers, and Lightning receives. All private key material and secret shares remain inside the Turnkey enclave throughout. + +| Activity | Description | +| -------- | ----------- | +| [`SPARK_SIGN_FROST`](/api-reference/activities/spark-sign-frost) | Generate a FROST partial signature for one or more Bitcoin transaction sighashes. Supports batching — branch + exit at deposit time, or up to 6 refund transactions per transfer. | +| [`SPARK_PREPARE_TRANSFER`](/api-reference/activities/spark-prepare-transfer) | Compute key tweaks, Feldman-split them for Spark Operators, ECIES-encrypt the receiver's new leaf key, and ECDSA-sign the transfer payload. | +| [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/spark-claim-transfer) | Verify the sender's signature, ECIES-decrypt the leaf key ciphertext, derive the receiver's new HD leaf key, and produce encrypted claim packages for Spark Operators. | +| [`SPARK_PREPARE_LIGHTNING_RECEIVE`](/api-reference/activities/spark-prepare-lightning-receive) | Generate a random payment preimage inside the enclave, Feldman-split it for Spark Operators, and return only the `paymentHash` for BOLT11 invoice construction. | + +Key derivation for deposit keys, signing keys, and static deposit keys uses the standard [`CREATE_WALLET_ACCOUNTS`](/api-reference/activities/create-wallet-accounts) activity with Spark-specific BIP-32 paths. + +## Flows + +For step-by-step details on every Turnkey-integrated Spark operation — including which activities to call, what parameters to pass, and what the enclave does at each step — see the [Spark flows](/networks/spark-flows) reference: + +- [Deposit: Bitcoin L1 → Spark](/networks/spark-flows#1-deposit-bitcoin-l1--spark) +- [Cooperative Withdrawal: Spark → Bitcoin L1](/networks/spark-flows#2-cooperative-withdrawal-spark--bitcoin-l1) +- [Unilateral Exit (Emergency)](/networks/spark-flows#3-unilateral-exit-spark--bitcoin-l1-emergency) +- [Initiate Transfer: Spark → Spark](/networks/spark-flows#4-initiate-transfer-spark--spark-sender) +- [Claim Transfer: Spark → Spark](/networks/spark-flows#5-claim-transfer-spark--spark-receiver) +- [Lightning Receive](/networks/spark-flows#6-lightning-receive) +- [Static Deposit: Bitcoin L1 → Spark](/networks/spark-flows#7-static-deposit-bitcoin-l1--spark-reusable-address) +- [Static Deposit Claim](/networks/spark-flows#8-static-deposit-claim) + ## SDK Example - [`examples/with-spark-schnorr`](https://github.com/tkhq/sdk/tree/main/examples/with-spark-schnorr): demonstrates wallet initialization, SO authentication and token minting + sending on Spark using Turnkey! From 3e740812fe26e7dba5297c7bf9b93b8b3de11064 Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Wed, 6 May 2026 20:04:07 -0400 Subject: [PATCH 2/6] further spark specific context --- networks/spark-flows.mdx | 55 +++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/networks/spark-flows.mdx b/networks/spark-flows.mdx index e9c6be9c..7b9b06fa 100644 --- a/networks/spark-flows.mdx +++ b/networks/spark-flows.mdx @@ -10,6 +10,16 @@ For quick-reference code examples, see the [Spark network overview](/networks/sp --- +## Understanding Spark Operators (SOs) + +Spark leaves are jointly controlled by your identity key and the Spark Operator collective (the SE), using [FROST threshold signing](https://docs.spark.money/learn/frost-signing). SOs never hold your complete key — they hold threshold shares — and the protocol is designed around a [1-of-n trust assumption](https://docs.spark.money/learn/trust-model): as long as one operator is honest, funds cannot be stolen. The worst case if all operators are unavailable is that you cannot transact, not that funds are lost. + +The current operators are **Lightspark** and **Flashnet**. For definitions of SE, SO, and SSP (Spark Service Provider), see [Spark core concepts](https://docs.spark.money/spark/core-concepts). For a deeper protocol walkthrough, see the [Spark technical overview](https://www.spark.money/technical-overview). + +**SO communication happens outside Turnkey.** Steps labeled **SO call** or **SSP call** throughout this page are direct client-to-operator API calls — Turnkey is not involved in those steps. Turnkey only handles the enclave key operations (`SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER`, etc.). For a working example of how to wire the Turnkey signer into the Spark SDK, refer to the JS code example provided with your Spark integration. + +--- + ## 1. Deposit: Bitcoin L1 → Spark **Turnkey activities:** `CREATE_WALLET_ACCOUNTS` ×1, `SPARK_SIGN_FROST` ×1 (batch of 2) @@ -34,7 +44,7 @@ The response contains the compressed deposit public key. This account now exists ### Step 2 — Get SO public key shares -Call the Spark Operators (SOs): "I want to start a deposit." The SOs return their key share for this leaf. +> **SO call:** Request SO public key shares for this deposit leaf. The SOs respond with their key share, which you combine with your deposit public key to compute the aggregate Taproot address. See [Deposits from L1](https://docs.spark.money/learn/deposits). ### Step 3 — Compute aggregate public key and Taproot deposit address @@ -46,7 +56,7 @@ Client-side: build unsigned `Txn1` (branch) and `Txn2` (exit, timelocked) using ### Step 5 — Get SO signing commitments -Call the SOs: "I need to sign a branch transaction and an exit transaction." The SOs return two sets of `operatorCommitments[]`. +> **SO call:** Request FROST nonce commitments for the branch and exit transaction signing sessions. The SOs return two sets of `operatorCommitments[]`. See [Deposits from L1](https://docs.spark.money/learn/deposits). ### Step 6 — FROST sign branch and exit transactions @@ -98,7 +108,7 @@ Client-side: build an unsigned Bitcoin L1 transaction spending from the leaf's T ### Step 2 — Get SO signing commitments -Call the SOs: "I want to cooperatively withdraw leaf X." The SOs return `operatorCommitments[]`. +> **SO call:** Request FROST nonce commitments for the cooperative withdrawal signing session. The SOs return `operatorCommitments[]`. See [Withdrawals](https://docs.spark.money/learn/withdrawals). ### Step 3 — FROST sign withdrawal transaction @@ -151,7 +161,7 @@ Wait approximately 100 blocks (~16 hours). Broadcast `Txn2`. BTC arrives at the user's L1 address. - The unilateral exit path does not require any Turnkey API calls because the exit transactions were fully signed at deposit time using `SPARK_SIGN_FROST`. + The unilateral exit path requires no Turnkey API calls and no SO cooperation — exit transactions were fully signed at deposit time using `SPARK_SIGN_FROST` and can be broadcast directly to Bitcoin L1. This is the property that makes SO unavailability a liveness concern rather than a safety concern. See [Trust model](https://docs.spark.money/learn/trust-model). --- @@ -162,11 +172,11 @@ Broadcast `Txn2`. BTC arrives at the user's L1 address. ### Step 1 — Select input leaves -Query the SOs for the sender's active leaves and select which cover the transfer amount. No Turnkey call. +> **SO call:** Query the SOs for the sender's active leaf set to determine which leaves cover the transfer amount. See [Transfers](https://docs.spark.money/learn/transfers). ### Step 2 — Get SO signing commitments for all new exit transactions -Call the SOs: "I need to sign refund transactions for N new leaves." The SOs return `operatorCommitments[]` for each of 3 refund transaction types per new leaf (CPFP, direct, direct-from-CPFP). For 2 new leaves (recipient leaf + change leaf) this yields 6 sets of commitments. +> **SO call:** Request FROST nonce commitments for refund transactions across all new leaves. The SOs return `operatorCommitments[]` for each of 3 refund transaction types per new leaf (CPFP, direct, direct-from-CPFP). For 2 new leaves (recipient leaf + change leaf) this yields 6 sets of commitments. See [FROST signing](https://docs.spark.money/learn/frost-signing). ### Step 3 — FROST sign all refund transactions @@ -267,12 +277,7 @@ Returns `operatorPackages[]` and `transferUserSignature`. ### Step 5 — Submit to Spark Operators -Send to the SOs: -- The aggregated refund signatures from Step 3 -- The encrypted operator packages from Step 4 -- The `transferUserSignature` from Step 4 - -The SOs verify, rotate key shares, delete old shares, store the ECIES blob for Bob, register new leaves, and activate watchtowers. +> **SO call:** Send the aggregated refund signatures from Step 3, the encrypted operator packages from Step 4, and the `transferUserSignature` from Step 4 to each SO. The SOs verify, rotate key shares, delete old shares, store the ECIES blob for Bob, register new leaves, and activate watchtowers. See [Transfers](https://docs.spark.money/learn/transfers) and [Trust model](https://docs.spark.money/learn/trust-model). --- @@ -282,7 +287,7 @@ The SOs verify, rotate key shares, delete old shares, store the ECIES blob for B ### Step 1 — Poll SOs for pending transfers -Call the SOs: "Any pending transfers for me?" The SOs return the encrypted blob, leaf ID, transfer ID, and sender identity public key. No Turnkey call. +> **SO call:** Query the SOs for pending inbound transfers. The SOs return the encrypted blob, leaf ID, transfer ID, and sender identity public key for each pending transfer. See [Transfers](https://docs.spark.money/learn/transfers). ### Step 2 — Claim the transfer @@ -322,7 +327,7 @@ Returns `operatorPackages[]`. ### Step 3 — Submit claim packages to SOs -Send the encrypted packages to the SOs. Each SO applies the tweak, rotating the leaf key to Bob's HD-derived key. +> **SO call:** Send the encrypted claim packages to each SO. Each SO applies the tweak, rotating the leaf key to Bob's HD-derived key. See [Transfers](https://docs.spark.money/learn/transfers). ### Step 4 — Store new leaf in local wallet state @@ -361,7 +366,7 @@ Returns `operatorPackages[]` and `paymentHash`. The raw preimage never leaves th ### Step 2 — Submit preimage shares to SOs -Send the encrypted operator packages to the SOs. Each SO stores its share and will contribute to preimage reconstruction when the Lightning payment arrives. +> **SO call:** Send the encrypted operator packages to each SO. Each SO stores its share and will contribute to preimage reconstruction when the Lightning payment arrives. ### Step 3 — Create Lightning invoice @@ -369,23 +374,23 @@ Construct a BOLT11 invoice encoding `paymentHash`. Share this invoice with the p ### Step 4 — Lock leaves to the SSP -Tell the Spark Service Provider (SSP): "I'll transfer X sats of my leaves to you if you pay this invoice." The SOs lock the specified leaves. +> **SSP call:** Notify the Spark Service Provider that you will transfer X sats of your leaves in exchange for payment of the invoice. The SOs lock the specified leaves. See [Spark core concepts](https://docs.spark.money/spark/core-concepts) for the SSP role. ### Step 5 — SSP pays the Lightning invoice -The SSP routes the payment across the Lightning Network. +> **SSP call:** The SSP routes the payment across the Lightning Network. ### Step 6 — SOs reveal preimage to SSP -Threshold SOs reconstruct the preimage from their Feldman shares and give it to the SSP. +> **SO call:** Once the incoming Lightning payment is detected, threshold SOs reconstruct the preimage from their Feldman shares and reveal it to the SSP. ### Step 7 — SSP completes the Lightning payment -The SSP reveals the preimage to the payer's node. The Lightning payment settles. +> **SSP call:** The SSP reveals the preimage to the payer's node. The Lightning payment settles. ### Step 8 — SOs transfer leaves to the SSP -The SOs atomically execute the leaf transfer from the receiver to the SSP. +> **SO call:** The SOs atomically execute the leaf transfer from the receiver to the SSP. ### Timeout path @@ -419,7 +424,7 @@ This is a deterministic, reusable key — call once and reuse the same address f ### Step 2 — Get SO public key shares -Call the SOs: "I want to register a static deposit address at index 0." The SOs return their key share for this index. +> **SO call:** Request SO public key shares for this static deposit index. The SOs respond with their key share, which you combine with your static deposit public key to compute the aggregate Taproot address. See [Deposits from L1](https://docs.spark.money/learn/deposits). ### Step 3 — Compute aggregate public key and Taproot deposit address @@ -431,7 +436,7 @@ Client-side: build unsigned `Txn1` (branch) and `Txn2` (exit, timelocked) using ### Step 5 — Get SO signing commitments -Call the SOs: "I need to sign branch and exit transactions for static deposit index 0." The SOs return two sets of `operatorCommitments[]`. +> **SO call:** Request FROST nonce commitments for the branch and exit transaction signing sessions at this static deposit index. The SOs return two sets of `operatorCommitments[]`. See [Deposits from L1](https://docs.spark.money/learn/deposits). ### Step 6 — FROST sign branch and exit transactions @@ -483,11 +488,9 @@ The SOs register a new Spark leaf for each confirmed deposit to this address. ### Step 1 — Confirmed deposit detected -After a deposit to the static address confirms on-chain, the SOs register a new leaf. Fetch an SSP quote: +After a deposit to the static address confirms on-chain, the SOs register a new leaf. -```typescript -const quote = await wallet.getClaimStaticDepositQuote(txid, outputIndex); -``` +> **SSP call:** Fetch a fee quote for the claim: `wallet.getClaimStaticDepositQuote(txid, outputIndex)`. ### Step 2 — Export static deposit key From 24d35b19590a025ad068a5a57ad5e559c082705e Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Mon, 11 May 2026 13:10:44 -0400 Subject: [PATCH 3/6] fix spark flows --- networks/spark-flows.mdx | 100 +++++++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 26 deletions(-) diff --git a/networks/spark-flows.mdx b/networks/spark-flows.mdx index 7b9b06fa..b0b46068 100644 --- a/networks/spark-flows.mdx +++ b/networks/spark-flows.mdx @@ -4,17 +4,23 @@ sidebarTitle: "Flows" description: "Step-by-step reference for every Turnkey-integrated Spark operation: deposits, withdrawals, transfers, Lightning receives, and exits." --- -This page documents each Spark flow in detail, showing exactly which Turnkey API activities are called at each step and what happens inside the enclave. All private key material and secret shares remain inside the Turnkey enclave throughout — they never reach client code. +This page documents each Spark flow in detail, showing exactly which Turnkey API activities are called at each step and what happens inside the enclave. In almost all flows, private key material and secret shares remain inside the Turnkey enclave and never reach client code. + +> **Exception — Static Deposits:** The [Static Deposit Setup flow](#7-static-deposit-bitcoin-l1--spark-reusable-address) requires exporting the static deposit private key from the enclave so the SSP can co-sign deposits while your wallet is offline. This is the only Spark flow that uses `EXPORT_WALLET_ACCOUNT`. See [Step 2 of Flow 7](#step-2--export-static-deposit-key-to-ssp) for the full security note. For quick-reference code examples, see the [Spark network overview](/networks/spark). --- -## Understanding Spark Operators (SOs) +## Spark Context: Understanding SOs, SSPs, and the SE Spark leaves are jointly controlled by your identity key and the Spark Operator collective (the SE), using [FROST threshold signing](https://docs.spark.money/learn/frost-signing). SOs never hold your complete key — they hold threshold shares — and the protocol is designed around a [1-of-n trust assumption](https://docs.spark.money/learn/trust-model): as long as one operator is honest, funds cannot be stolen. The worst case if all operators are unavailable is that you cannot transact, not that funds are lost. -The current operators are **Lightspark** and **Flashnet**. For definitions of SE, SO, and SSP (Spark Service Provider), see [Spark core concepts](https://docs.spark.money/spark/core-concepts). For a deeper protocol walkthrough, see the [Spark technical overview](https://www.spark.money/technical-overview). +- **SO (Spark Operator):** One node in the operator collective that holds a threshold key share for every leaf. The current operators are **Lightspark** and **Flashnet**. +- **SE (Spark Entity):** The collective of all SOs acting together. FROST threshold signing requires a quorum of the SE to authorise any leaf operation. +- **SSP (Spark Service Provider):** An application-layer service (e.g. a wallet backend or exchange) that coordinates flows on your behalf — routing Lightning payments, processing static deposits, and submitting transfers. The SSP has no key-share authority over leaves; it relies on the SOs for that. + +For full definitions see [Spark core concepts](https://docs.spark.money/spark/core-concepts). For a deeper protocol walkthrough, see the [Spark technical overview](https://www.spark.money/technical-overview). **SO communication happens outside Turnkey.** Steps labeled **SO call** or **SSP call** throughout this page are direct client-to-operator API calls — Turnkey is not involved in those steps. Turnkey only handles the enclave key operations (`SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER`, etc.). For a working example of how to wire the Turnkey signer into the Spark SDK, refer to the JS code example provided with your Spark integration. @@ -88,6 +94,8 @@ The enclave generates fresh nonces and returns 2 `(signatureShare, hiding, bindi Store `Txn1` and `Txn2` securely. These are the permanent unilateral exit path — see [Unilateral Exit](#3-unilateral-exit-spark--bitcoin-l1-emergency). +> **Security note:** These two transactions are your primary protection against Spark Operator misbehaviour or prolonged downtime. They were signed inside the Turnkey enclave at deposit time and require no SO cooperation to broadcast — if the entire operator set goes offline or acts maliciously, you can still recover your BTC by broadcasting them directly to Bitcoin L1. Treat them with the same care as a seed phrase: store them in durable, encrypted, off-device storage and back them up. If they are lost and the SOs are unavailable, your only recovery path is gone. + ### Step 8 — Broadcast deposit transaction Broadcast a standard Bitcoin transaction sending BTC to the `bc1p...` Taproot address. @@ -102,6 +110,8 @@ The SOs register the new Spark leaf once the transaction confirms. **Turnkey activities:** `SPARK_SIGN_FROST` ×1 +A cooperative withdrawal is the normal, fast path for moving BTC from a Spark leaf back to a Bitcoin L1 address. It requires the SOs to co-sign a single withdrawal transaction and settles in one on-chain confirmation. If the SOs are unavailable, use the [Unilateral Exit](#3-unilateral-exit-spark--bitcoin-l1-emergency) path instead — it is slower (~16 hours timelock) but requires no operator cooperation. + ### Step 1 — Construct withdrawal transaction Client-side: build an unsigned Bitcoin L1 transaction spending from the leaf's Taproot address to the user's personal Bitcoin address. @@ -142,7 +152,7 @@ BTC arrives at the user's L1 address. The SOs deactivate the leaf. **Turnkey activities:** None -This path uses transactions that were pre-signed at deposit time. No Turnkey API calls are required. +Use this path when cooperative withdrawal is not possible — for example, if the Spark Operators are offline, unresponsive, or you have reason to believe they are acting maliciously. It requires no SO cooperation and no Turnkey API calls, but it is subject to an approximately 100-block (~16 hour) relative timelock before funds are spendable. This path relies entirely on the exit transactions that were pre-signed at deposit time (Flow 1 Step 7 / Flow 7 Step 8); if those transactions were not stored, this recovery path is unavailable. ### Step 1 — Retrieve pre-signed exit transactions @@ -400,7 +410,7 @@ If the SSP doesn't pay within the time limit, the SOs unlock the receiver's leav ## 7. Static Deposit: Bitcoin L1 → Spark (Reusable Address) -**Turnkey activities:** `CREATE_WALLET_ACCOUNTS` ×1 (once per index, reused thereafter), `SPARK_SIGN_FROST` ×1 (batch of 2) +**Turnkey activities:** `CREATE_WALLET_ACCOUNTS` ×1, `EXPORT_WALLET_ACCOUNT` ×1, `SPARK_SIGN_FROST` ×1 (batch of 2) A static deposit address is deterministic and permanent — the same path at index N always produces the same Taproot address and can receive multiple deposits, each creating a separate Spark leaf. @@ -422,23 +432,42 @@ Call [`CREATE_WALLET_ACCOUNTS`](/api-reference/activities/create-wallet-accounts This is a deterministic, reusable key — call once and reuse the same address for all future deposits at that index. -### Step 2 — Get SO public key shares +### Step 2 — Export static deposit key to SSP + +Call [`EXPORT_WALLET_ACCOUNT`](/api-reference/activities/export-wallet-account) to retrieve the private key in a P256-encrypted bundle: + +```json +{ + "address": "", + "targetPublicKey": "" +} +``` + +Decrypt the export bundle locally using the ephemeral P256 private key, then send the raw key to the SSP. The SSP stores this key so it can co-sign deposit processing while you are offline. + +> **Security note:** This is the only Spark flow that requires exporting a raw private key from the Turnkey enclave. From the moment the key is shared with the SSP until you claim any deposits, the SSP holds co-signing capability on this address. This is the intentional custodial tradeoff of static deposits — the SSP needs the key to process deposits while your wallet is offline. To minimise exposure: +> - Use a fresh ephemeral P256 keypair for each export and zero it immediately after decrypting +> - Send the key to the SSP over an encrypted channel +> - Zero the key in your local memory immediately after it has been transmitted +> - Only use SSPs you trust; a compromised SSP with this key could co-sign spends from your static deposit address + +### Step 3 — Get SO public key shares > **SO call:** Request SO public key shares for this static deposit index. The SOs respond with their key share, which you combine with your static deposit public key to compute the aggregate Taproot address. See [Deposits from L1](https://docs.spark.money/learn/deposits). -### Step 3 — Compute aggregate public key and Taproot deposit address +### Step 4 — Compute aggregate public key and Taproot deposit address Client-side: `P_aggregate = P_static_deposit_user + P_so`. Derive the permanent `bc1p...` Taproot address for this index. -### Step 4 — Construct branch and exit transactions +### Step 5 — Construct branch and exit transactions Client-side: build unsigned `Txn1` (branch) and `Txn2` (exit, timelocked) using the aggregate public key. -### Step 5 — Get SO signing commitments +### Step 6 — Get SO signing commitments > **SO call:** Request FROST nonce commitments for the branch and exit transaction signing sessions at this static deposit index. The SOs return two sets of `operatorCommitments[]`. See [Deposits from L1](https://docs.spark.money/learn/deposits). -### Step 6 — FROST sign branch and exit transactions +### Step 7 — FROST sign branch and exit transactions Call [`SPARK_SIGN_FROST`](/api-reference/activities/spark-sign-frost) using `SPARK_KEY_TYPE_STATIC_DEPOSIT_HD` and the index: @@ -464,15 +493,15 @@ Call [`SPARK_SIGN_FROST`](/api-reference/activities/spark-sign-frost) using `SPA Returns 2 `(signatureShare, hiding, binding)` tuples. The client aggregates with SO partial signatures to produce 2 final Schnorr signatures. -### Step 7 — Store signed exit transactions +### Step 8 — Store signed exit transactions Store `Txn1` and `Txn2`. Each individual deposit to this address generates a separate leaf with its own exit transactions. -### Step 8 — Broadcast deposit transaction +### Step 9 — Broadcast deposit transaction Broadcast a standard Bitcoin transaction sending BTC to the static `bc1p...` Taproot address. -### Step 9 — Wait for L1 confirmation +### Step 10 — Wait for L1 confirmation The SOs register a new Spark leaf for each confirmed deposit to this address. @@ -480,11 +509,9 @@ The SOs register a new Spark leaf for each confirmed deposit to this address. ## 8. Static Deposit Claim -**Turnkey activities:** `EXPORT_WALLET_ACCOUNT` ×1 +**Turnkey activities:** `SPARK_CLAIM_TRANSFER` ×1 - - The Spark SDK's static deposit claim path requires the raw private key to sign the claim transaction directly. Turnkey exports the key into a P256-encrypted bundle that only your local session can decrypt. The key is held in memory only for the duration of the claim. - +Once a deposit is confirmed, the SSP packages the leaf key (encrypted to your identity public key) as a standard transfer claim. The claim goes through the same `SPARK_CLAIM_TRANSFER` path as any other incoming Spark transfer — no key export required. ### Step 1 — Confirmed deposit detected @@ -492,24 +519,45 @@ After a deposit to the static address confirms on-chain, the SOs register a new > **SSP call:** Fetch a fee quote for the claim: `wallet.getClaimStaticDepositQuote(txid, outputIndex)`. -### Step 2 — Export static deposit key +### Step 2 — Claim the transfer -Call [`EXPORT_WALLET_ACCOUNT`](/api-reference/activities/export-wallet-account) to retrieve the private key in a P256-encrypted bundle that only the local session can decrypt: +Call [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/spark-claim-transfer): ```json { - "address": "", - "targetPublicKey": "" + "signWith": "spark1p...", + "packageRequest": { + "claim": { + "transferId": "", + "senderIdentityPublicKey": "", + "threshold": 2, + "operatorRecipients": [ + { "operatorId": "SO1", "encryptionPublicKey": "02abc..." }, + { "operatorId": "SO2", "encryptionPublicKey": "02def..." } + ], + "leaves": [{ + "leafId": "", + "ciphertext": "", + "senderSignature": "" + }] + } + } } ``` -The client decrypts the export bundle locally using the ephemeral P256 private key. +The enclave atomically: +- Verifies the SSP's per-leaf ECDSA signature +- ECIES-decrypts the ciphertext using your identity private key to recover the current leaf private key +- Derives your new leaf key from your HD wallet at `m/8797555'/n'/1'/{leafIndex}'` +- Computes `claim_tweak = current_leaf_priv − new_leaf_priv` +- Feldman-splits the claim tweak for the SOs +- ECIES-encrypts each SO's claim share to their encryption public key -### Step 3 — Claim the static deposit +Returns `operatorPackages[]`. + +### Step 3 — Submit claim packages to SOs -1. Install the decrypted private key: `signer.setStaticDepositSecretKey(0, key)` -2. Call `wallet.claimStaticDeposit({ transactionId, creditAmountSats, sspSignature })` -3. Immediately zero the key: `signer.clearStaticDepositSecretKey(0)` +> **SO call:** Send the encrypted packages to the SOs. Each SO applies the tweak, rotating the leaf key to your HD-derived key. After this step, the leaf is fully under your control — the SSP no longer has co-signing capability on this leaf. ### Step 4 — Wait for balance availability From 5b6660e2dc03cbe326c7ffd8215dadc4dc7139cb Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Mon, 11 May 2026 13:39:11 -0400 Subject: [PATCH 4/6] documentation minor fixes --- networks/spark-flows.mdx | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/networks/spark-flows.mdx b/networks/spark-flows.mdx index b0b46068..f033d2d3 100644 --- a/networks/spark-flows.mdx +++ b/networks/spark-flows.mdx @@ -46,7 +46,9 @@ Call [`CREATE_WALLET_ACCOUNTS`](/api-reference/activities/create-wallet-accounts } ``` -The response contains the compressed deposit public key. This account now exists in Turnkey and can be referenced in future signing calls. +The response contains the compressed deposit public key. This account now exists in Turnkey and can be referenced in future signing calls. Each deposit key should only receive a single deposit — the exit transactions signed in Step 6 reference a specific UTXO, so a second deposit to the same address would arrive without pre-signed exit protection. + +Replace `0'/2'` with `N'/2'` where `N` is your account index. The path `m/8797555'/0'/2'` is for account 0. ### Step 2 — Get SO public key shares @@ -144,7 +146,7 @@ Attach the final signature and broadcast to Bitcoin L1. ### Step 5 — Wait for confirmation -BTC arrives at the user's L1 address. The SOs deactivate the leaf. +BTC arrives at the user's L1 address. The SOs deactivate the leaf. The pre-signed exit transactions stored at deposit time are now spent and can be discarded — the UTXO no longer exists on L1. --- @@ -182,7 +184,7 @@ Broadcast `Txn2`. BTC arrives at the user's L1 address. ### Step 1 — Select input leaves -> **SO call:** Query the SOs for the sender's active leaf set to determine which leaves cover the transfer amount. See [Transfers](https://docs.spark.money/learn/transfers). +> **SO call:** Query the SOs for the sender's active leaf set to determine which leaves cover the transfer amount. The SOs are the authoritative source of leaf state — unlike a standard Bitcoin wallet, the client does not maintain a local UTXO set independently. See [Transfers](https://docs.spark.money/learn/transfers). ### Step 2 — Get SO signing commitments for all new exit transactions @@ -278,7 +280,7 @@ Call [`SPARK_PREPARE_TRANSFER`](/api-reference/activities/spark-prepare-transfer The enclave atomically: 1. Derives old and new leaf keys from the identity key's HD wallet 2. Computes `tweak = oldKey − newKey` per leaf -3. Feldman-splits each tweak for the SOs +3. Feldman VSS-splits each tweak for the SOs — each SO receives a share plus verifiable commitments, allowing them to independently verify their share is correct without seeing other shares 4. ECIES-encrypts Bob's new leaf key to his identity public key 5. ECIES-encrypts each SO's Shamir share to their encryption public key 6. ECDSA-signs the per-leaf payload and the outer transfer payload with the identity key @@ -335,13 +337,13 @@ The enclave atomically: Returns `operatorPackages[]`. -### Step 3 — Submit claim packages to SOs +### Step 3 — Store new leaf in local wallet state -> **SO call:** Send the encrypted claim packages to each SO. Each SO applies the tweak, rotating the leaf key to Bob's HD-derived key. See [Transfers](https://docs.spark.money/learn/transfers). +Record the `leafId → derivation path` mapping locally as soon as `SPARK_CLAIM_TRANSFER` returns — before submitting to the SOs. If the SO submission fails, you can retry; if local storage is deferred and something fails before it runs, the leaf becomes untracked. -### Step 4 — Store new leaf in local wallet state +### Step 4 — Submit claim packages to SOs -Record the `leafId → derivation path` mapping locally. Bob's wallet now tracks this leaf. +> **SO call:** Send the encrypted claim packages to each SO. Each SO applies the tweak, rotating the leaf key to Bob's HD-derived key. See [Transfers](https://docs.spark.money/learn/transfers). --- @@ -432,6 +434,8 @@ Call [`CREATE_WALLET_ACCOUNTS`](/api-reference/activities/create-wallet-accounts This is a deterministic, reusable key — call once and reuse the same address for all future deposits at that index. +Replace `0'/3'/0'` with `N'/3'/0'` where `N` is your account index, and increment the final segment for additional static addresses at the same account. + ### Step 2 — Export static deposit key to SSP Call [`EXPORT_WALLET_ACCOUNT`](/api-reference/activities/export-wallet-account) to retrieve the private key in a P256-encrypted bundle: @@ -443,7 +447,7 @@ Call [`EXPORT_WALLET_ACCOUNT`](/api-reference/activities/export-wallet-account) } ``` -Decrypt the export bundle locally using the ephemeral P256 private key, then send the raw key to the SSP. The SSP stores this key so it can co-sign deposit processing while you are offline. +Decrypt the export bundle locally using the ephemeral P256 private key, then transmit the raw key to the SSP via their API. This call is made directly to the SSP — it is outside Turnkey's API surface. The SSP stores this key so it can co-sign deposit processing while your wallet is offline. > **Security note:** This is the only Spark flow that requires exporting a raw private key from the Turnkey enclave. From the moment the key is shared with the SSP until you claim any deposits, the SSP holds co-signing capability on this address. This is the intentional custodial tradeoff of static deposits — the SSP needs the key to process deposits while your wallet is offline. To minimise exposure: > - Use a fresh ephemeral P256 keypair for each export and zero it immediately after decrypting @@ -521,6 +525,8 @@ After a deposit to the static address confirms on-chain, the SOs register a new ### Step 2 — Claim the transfer +The SSP appears as the `senderIdentityPublicKey` here because it is the SSP — not the original L1 depositor — that ECIES-encrypted the leaf key to your identity pubkey when it processed the incoming deposit. The original Bitcoin sender has no role in the Spark claim flow. + Call [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/spark-claim-transfer): ```json From f6cd7e03828298674586afc990ce68253e56caf1 Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Mon, 11 May 2026 13:49:36 -0400 Subject: [PATCH 5/6] update key derivations --- .../activities/spark-prepare-transfer.mdx | 18 ++++--- api-reference/activities/spark-sign-frost.mdx | 53 ++++++++++++------- networks/spark-flows.mdx | 32 +++++------ 3 files changed, 61 insertions(+), 42 deletions(-) diff --git a/api-reference/activities/spark-prepare-transfer.mdx b/api-reference/activities/spark-prepare-transfer.mdx index ce427ce0..3254edf0 100644 --- a/api-reference/activities/spark-prepare-transfer.mdx +++ b/api-reference/activities/spark-prepare-transfer.mdx @@ -66,10 +66,10 @@ Unique identifier for a given Organization. The identifier of the leaf being transferred. - Derivation descriptor for the sender's current leaf key. Must use `type: SPARK_KEY_TYPE_SIGNING_HD` with the current leaf's `leafId`. The enclave derives the current leaf private key from this path to compute the tweak. + Derivation descriptor for the sender's current leaf key. Must use `signingLeaf` with the current leaf's `leafId` — e.g. `{ "signingLeaf": { "leafId": "" } }`. The enclave derives the current leaf private key to compute the tweak. - Derivation descriptor for the receiver's new leaf key. Must use `type: SPARK_KEY_TYPE_SIGNING_HD` with the new leaf's `leafId`. The enclave derives this key and encrypts it to the receiver's identity public key. + Derivation descriptor for the receiver's new leaf key. Must use `signingLeaf` with the new leaf's `leafId` — e.g. `{ "signingLeaf": { "leafId": "" } }`. The enclave derives this key and encrypts it to the receiver's identity public key. The fully aggregated Schnorr signature for the new leaf's CPFP refund transaction, hex-encoded. Produced by aggregating the partial signatures from `SPARK_SIGN_FROST` with the Spark Operators' shares. @@ -199,12 +199,14 @@ curl --request POST \ { "leafId": "", "oldLeafDerivation": { - "type": "SPARK_KEY_TYPE_SIGNING_HD", - "leafId": "" + "signingLeaf": { + "leafId": "" + } }, "newLeafDerivation": { - "type": "SPARK_KEY_TYPE_SIGNING_HD", - "leafId": "" + "signingLeaf": { + "leafId": "" + } }, "refundSignature": "", "directRefundSignature": "", @@ -238,8 +240,8 @@ const response = await turnkeyClient.apiClient().sparkPrepareTransfer({ ], leaves: [{ leafId: "", - oldLeafDerivation: { type: "SPARK_KEY_TYPE_SIGNING_HD", leafId: "" }, - newLeafDerivation: { type: "SPARK_KEY_TYPE_SIGNING_HD", leafId: "" }, + oldLeafDerivation: { signingLeaf: { leafId: "" } }, + newLeafDerivation: { signingLeaf: { leafId: "" } }, refundSignature: "", directRefundSignature: "", directFromCpfpRefundSignature: "", diff --git a/api-reference/activities/spark-sign-frost.mdx b/api-reference/activities/spark-sign-frost.mdx index defe33bf..1ae3f8fc 100644 --- a/api-reference/activities/spark-sign-frost.mdx +++ b/api-reference/activities/spark-sign-frost.mdx @@ -40,24 +40,41 @@ Unique identifier for a given Organization.

A batch of one or more signature requests. A single call can sign multiple transactions (e.g. branch + exit at deposit time, or up to 6 refund transactions per transfer).

- The key derivation descriptor that identifies which leaf key to use for this signature. - - - Selects the key derivation strategy. + A oneof descriptor — set exactly one of the following keys. The selected key tells the enclave which private key to use for this signature. - | Value | Required Field | Used For | - | --- | --- | --- | - | `SPARK_KEY_TYPE_DEPOSIT` | _(none)_ | Single-use deposit transactions | - | `SPARK_KEY_TYPE_STATIC_DEPOSIT_HD` | `index` | Static (reusable) deposit transactions | - | `SPARK_KEY_TYPE_SIGNING_HD` | `leafId` | Active leaf signing — withdrawals, transfer refunds | + | Key | Shape | Used For | + | --- | --- | --- | + | `identity` | `{}` | Spark identity key | + | `signingLeaf` | `{ "leafId": "..." }` | Active leaf signing — withdrawals, transfer refunds | + | `deposit` | `{}` | Single-use deposit transactions | + | `staticDeposit` | `{ "index": N }` | Static (reusable) deposit transactions | + | `htlcPreimage` | `{}` | HTLC preimage key | - Enum options: `SPARK_KEY_TYPE_DEPOSIT`, `SPARK_KEY_TYPE_STATIC_DEPOSIT_HD`, `SPARK_KEY_TYPE_SIGNING_HD` + + + Empty object `{}`. Set this key to sign with the Spark identity key. + + + Set this key to sign with an active leaf's HD-derived signing key. + + + Unique identifier of the Spark leaf whose signing key should be used. + + - - Required when `type` is `SPARK_KEY_TYPE_SIGNING_HD`. Identifies the specific Spark leaf whose HD-derived signing key should be used. + + Empty object `{}`. Set this key to sign with the single-use deposit key derived at deposit time. - - Required when `type` is `SPARK_KEY_TYPE_STATIC_DEPOSIT_HD`. The integer index of the static deposit address (e.g. `0` for the first static deposit address at path `m/8797555'/{account}'/3'/0'`). + + Set this key to sign with the static deposit key at the given index. + + + The integer index of the static deposit address (e.g. `0` for path `m/8797555'/{account}'/3'/0'`). + + + + + Empty object `{}`. Set this key to sign with the HTLC preimage key. @@ -189,8 +206,9 @@ curl --request POST \ "signatures": [ { "derivation": { - "type": "SPARK_KEY_TYPE_SIGNING_HD", - "leafId": "" + "signingLeaf": { + "leafId": "" + } }, "message": "", "verifyingKey": "", @@ -221,8 +239,7 @@ const response = await turnkeyClient.apiClient().sparkSignFrost({ signWith: "", signatures: [{ derivation: { - type: "SPARK_KEY_TYPE_SIGNING_HD", - leafId: "", + signingLeaf: { leafId: "" }, }, message: "", verifyingKey: "", diff --git a/networks/spark-flows.mdx b/networks/spark-flows.mdx index f033d2d3..c4cc2685 100644 --- a/networks/spark-flows.mdx +++ b/networks/spark-flows.mdx @@ -75,13 +75,13 @@ Call [`SPARK_SIGN_FROST`](/api-reference/activities/spark-sign-frost) with a bat "signWith": "spark1pgss...", "signatures": [ { - "derivation": { "type": "SPARK_KEY_TYPE_DEPOSIT" }, + "derivation": { "deposit": {} }, "message": "", "operatorCommitments": [{ "id": "SO1", "hiding": "", "binding": "" }], "verifyingKey": "" }, { - "derivation": { "type": "SPARK_KEY_TYPE_DEPOSIT" }, + "derivation": { "deposit": {} }, "message": "", "operatorCommitments": [{ "id": "SO1", "hiding": "", "binding": "" }], "verifyingKey": "" @@ -130,7 +130,7 @@ Call [`SPARK_SIGN_FROST`](/api-reference/activities/spark-sign-frost): { "signWith": "spark1pgss...", "signatures": [{ - "derivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "leaf-abc-123" }, + "derivation": { "signingLeaf": { "leafId": "leaf-abc-123" } }, "message": "", "operatorCommitments": [{ "id": "SO1", "hiding": "", "binding": "" }], "verifyingKey": "" @@ -199,37 +199,37 @@ Call [`SPARK_SIGN_FROST`](/api-reference/activities/spark-sign-frost) with a bat "signWith": "spark1pgss...", "signatures": [ { - "derivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "bob-new-leaf-id" }, + "derivation": { "signingLeaf": { "leafId": "bob-new-leaf-id" } }, "message": "", "operatorCommitments": [...], "verifyingKey": "" }, { - "derivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "bob-new-leaf-id" }, + "derivation": { "signingLeaf": { "leafId": "bob-new-leaf-id" } }, "message": "", "operatorCommitments": [...], "verifyingKey": "" }, { - "derivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "bob-new-leaf-id" }, + "derivation": { "signingLeaf": { "leafId": "bob-new-leaf-id" } }, "message": "", "operatorCommitments": [...], "verifyingKey": "" }, { - "derivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "alice-change-leaf-id" }, + "derivation": { "signingLeaf": { "leafId": "alice-change-leaf-id" } }, "message": "", "operatorCommitments": [...], "verifyingKey": "" }, { - "derivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "alice-change-leaf-id" }, + "derivation": { "signingLeaf": { "leafId": "alice-change-leaf-id" } }, "message": "", "operatorCommitments": [...], "verifyingKey": "" }, { - "derivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "alice-change-leaf-id" }, + "derivation": { "signingLeaf": { "leafId": "alice-change-leaf-id" } }, "message": "", "operatorCommitments": [...], "verifyingKey": "" @@ -258,16 +258,16 @@ Call [`SPARK_PREPARE_TRANSFER`](/api-reference/activities/spark-prepare-transfer "leaves": [ { "leafId": "old-leaf-id", - "oldLeafDerivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "old-leaf-id" }, - "newLeafDerivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "bob-new-leaf-id" }, + "oldLeafDerivation": { "signingLeaf": { "leafId": "old-leaf-id" } }, + "newLeafDerivation": { "signingLeaf": { "leafId": "bob-new-leaf-id" } }, "refundSignature": "", "directRefundSignature": "", "directFromCpfpRefundSignature": "" }, { "leafId": "old-leaf-id", - "oldLeafDerivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "old-leaf-id" }, - "newLeafDerivation": { "type": "SPARK_KEY_TYPE_SIGNING_HD", "leafId": "alice-change-leaf-id" }, + "oldLeafDerivation": { "signingLeaf": { "leafId": "old-leaf-id" } }, + "newLeafDerivation": { "signingLeaf": { "leafId": "alice-change-leaf-id" } }, "refundSignature": "", "directRefundSignature": "", "directFromCpfpRefundSignature": "" @@ -473,20 +473,20 @@ Client-side: build unsigned `Txn1` (branch) and `Txn2` (exit, timelocked) using ### Step 7 — FROST sign branch and exit transactions -Call [`SPARK_SIGN_FROST`](/api-reference/activities/spark-sign-frost) using `SPARK_KEY_TYPE_STATIC_DEPOSIT_HD` and the index: +Call [`SPARK_SIGN_FROST`](/api-reference/activities/spark-sign-frost) using the `staticDeposit` derivation key and the index: ```json { "signWith": "spark1pgss...", "signatures": [ { - "derivation": { "type": "SPARK_KEY_TYPE_STATIC_DEPOSIT_HD", "index": 0 }, + "derivation": { "staticDeposit": { "index": 0 } }, "message": "", "operatorCommitments": [...], "verifyingKey": "" }, { - "derivation": { "type": "SPARK_KEY_TYPE_STATIC_DEPOSIT_HD", "index": 0 }, + "derivation": { "staticDeposit": { "index": 0 } }, "message": "", "operatorCommitments": [...], "verifyingKey": "" From 2bb5c13eb7cd6e60af05f930dc13ac19e5384eb5 Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Mon, 11 May 2026 18:29:44 -0400 Subject: [PATCH 6/6] remove package request from claim transfer --- .../activities/spark-claim-transfer.mdx | 135 ++++++++---------- networks/spark-flows.mdx | 55 ++++--- 2 files changed, 89 insertions(+), 101 deletions(-) diff --git a/api-reference/activities/spark-claim-transfer.mdx b/api-reference/activities/spark-claim-transfer.mdx index b5627961..9f06af45 100644 --- a/api-reference/activities/spark-claim-transfer.mdx +++ b/api-reference/activities/spark-claim-transfer.mdx @@ -36,46 +36,41 @@ Unique identifier for a given Organization. The Spark address of the receiver's signing identity. The enclave uses the corresponding identity private key to ECIES-decrypt the leaf key ciphertext sent by the sender. - -

Wraps the claim descriptor.

- - -

The claim descriptor specifying the transfer to claim and the leaves to take ownership of.

- - - The UUID of the transfer to claim. Must match the `transferId` used by the sender in `SPARK_PREPARE_TRANSFER`. + +

The claim descriptor specifying the transfer to claim and the leaves to take ownership of.

+ + + The UUID of the transfer to claim. Must match the `transferId` used by the sender in `SPARK_PREPARE_TRANSFER`. + + + The sender's Spark identity public key, hex-encoded (compressed secp256k1). The enclave uses this to verify the sender's per-leaf ECDSA signature before decrypting. + + + The Shamir secret sharing threshold for splitting the claim tweak across Spark Operators. Must match the threshold used by the sender. + + +

The Spark Operators that will receive the receiver's encrypted claim tweak shares. Each operator's share is ECIES-encrypted to its individual encryption public key.

+ + + The Spark Operator identifier (e.g. `"SO1"`). - - The sender's Spark identity public key, hex-encoded (compressed secp256k1). The enclave uses this to verify the sender's per-leaf ECDSA signature before decrypting. + + The operator's ECIES encryption public key, hex-encoded. - - The Shamir secret sharing threshold for splitting the claim tweak across Spark Operators. Must match the threshold used by the sender. + +
+ +

The individual leaves being claimed. Each entry provides the ECIES-encrypted current leaf key (stored by the Spark Operators on the sender's behalf) and the sender's per-leaf authorization signature.

+ + + The identifier of the leaf being claimed. + + + The ECIES-encrypted blob containing the current leaf private key, hex-encoded. This blob was produced by the sender's enclave during `SPARK_PREPARE_TRANSFER` and stored by the Spark Operators. Retrieve it by polling the Spark Operators for pending transfers. + + + The sender's compact ECDSA signature over `SHA256(leafId || transferId || ciphertext)`, hex-encoded. The enclave verifies this signature against `senderIdentityPublicKey` before decrypting the ciphertext. - -

The Spark Operators that will receive the receiver's encrypted claim tweak shares. Each operator's share is ECIES-encrypted to its individual encryption public key.

- - - The Spark Operator identifier (e.g. `"SO1"`). - - - The operator's ECIES encryption public key, hex-encoded. - - -
- -

The individual leaves being claimed. Each entry provides the ECIES-encrypted current leaf key (stored by the Spark Operators on the sender's behalf) and the sender's per-leaf authorization signature.

- - - The identifier of the leaf being claimed. - - - The ECIES-encrypted blob containing the current leaf private key, hex-encoded. This blob was produced by the sender's enclave during `SPARK_PREPARE_TRANSFER` and stored by the Spark Operators. Retrieve it by polling the Spark Operators for pending transfers. - - - The sender's compact ECDSA signature over `SHA256(leafId || transferId || ciphertext)`, hex-encoded. The enclave verifies this signature against `senderIdentityPublicKey` before decrypting the ciphertext. - - -
@@ -116,8 +111,8 @@ The activity type The Spark address of the receiver's signing identity. - -The claim package request submitted. + +The claim descriptor submitted.
@@ -180,23 +175,21 @@ curl --request POST \ "organizationId": " (Your Organization ID)", "parameters": { "signWith": "", - "packageRequest": { - "claim": { - "transferId": "", - "senderIdentityPublicKey": "", - "threshold": 2, - "operatorRecipients": [ - { "operatorId": "SO1", "encryptionPublicKey": "" }, - { "operatorId": "SO2", "encryptionPublicKey": "" } - ], - "leaves": [ - { - "leafId": "", - "ciphertext": "", - "senderSignature": "" - } - ] - } + "claim": { + "transferId": "", + "senderIdentityPublicKey": "", + "threshold": 2, + "operatorRecipients": [ + { "operatorId": "SO1", "encryptionPublicKey": "" }, + { "operatorId": "SO2", "encryptionPublicKey": "" } + ], + "leaves": [ + { + "leafId": "", + "ciphertext": "", + "senderSignature": "" + } + ] } } }' @@ -214,21 +207,19 @@ const turnkeyClient = new Turnkey({ const response = await turnkeyClient.apiClient().sparkClaimTransfer({ signWith: "", - packageRequest: { - claim: { - transferId: "", - senderIdentityPublicKey: "", - threshold: 2, - operatorRecipients: [ - { operatorId: "SO1", encryptionPublicKey: "" }, - { operatorId: "SO2", encryptionPublicKey: "" }, - ], - leaves: [{ - leafId: "", - ciphertext: "", - senderSignature: "", - }], - }, + claim: { + transferId: "", + senderIdentityPublicKey: "", + threshold: 2, + operatorRecipients: [ + { operatorId: "SO1", encryptionPublicKey: "" }, + { operatorId: "SO2", encryptionPublicKey: "" }, + ], + leaves: [{ + leafId: "", + ciphertext: "", + senderSignature: "", + }], }, }); ``` @@ -254,7 +245,7 @@ const response = await turnkeyClient.apiClient().sparkClaimTransfer({ "intent": { "claimSparkTransferIntent": { "signWith": "", - "packageRequest": "" + "claim": "" } }, "result": { diff --git a/networks/spark-flows.mdx b/networks/spark-flows.mdx index c4cc2685..17d467dc 100644 --- a/networks/spark-flows.mdx +++ b/networks/spark-flows.mdx @@ -308,20 +308,19 @@ Call [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/spark-claim-transfer): ```json { "signWith": "spark1pbob...", - "packageRequest": { - "claim": { - "transferId": "aaaaa-bbbb-...", - "senderIdentityPublicKey": "", - "threshold": 2, - "operatorRecipients": [ - { "operatorId": "SO1", "encryptionPublicKey": "02abc..." }, - { "operatorId": "SO2", "encryptionPublicKey": "02def..." } - ], - "leaves": [{ - "leafId": "bob-new-leaf-id", - "ciphertext": "", - "senderSignature": "" - }] + "claim": { + "transferId": "aaaaa-bbbb-...", + "senderIdentityPublicKey": "", + "threshold": 2, + "operatorRecipients": [ + { "operatorId": "SO1", "encryptionPublicKey": "02abc..." }, + { "operatorId": "SO2", "encryptionPublicKey": "02def..." } + ], + "leaves": [{ + "leafId": "bob-new-leaf-id", + "ciphertext": "", + "senderSignature": "" + }] } } } @@ -532,21 +531,19 @@ Call [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/spark-claim-transfer): ```json { "signWith": "spark1p...", - "packageRequest": { - "claim": { - "transferId": "", - "senderIdentityPublicKey": "", - "threshold": 2, - "operatorRecipients": [ - { "operatorId": "SO1", "encryptionPublicKey": "02abc..." }, - { "operatorId": "SO2", "encryptionPublicKey": "02def..." } - ], - "leaves": [{ - "leafId": "", - "ciphertext": "", - "senderSignature": "" - }] - } + "claim": { + "transferId": "", + "senderIdentityPublicKey": "", + "threshold": 2, + "operatorRecipients": [ + { "operatorId": "SO1", "encryptionPublicKey": "02abc..." }, + { "operatorId": "SO2", "encryptionPublicKey": "02def..." } + ], + "leaves": [{ + "leafId": "", + "ciphertext": "", + "senderSignature": "" + }] } } ```