diff --git a/api-reference/activities/spark-claim-transfer.mdx b/api-reference/activities/spark-claim-transfer.mdx new file mode 100644 index 00000000..9f06af45 --- /dev/null +++ b/api-reference/activities/spark-claim-transfer.mdx @@ -0,0 +1,271 @@ +--- +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. + + + +

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 descriptor 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": "", + "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: "", + 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": "", + "claim": "" + } + }, + "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..3254edf0 --- /dev/null +++ b/api-reference/activities/spark-prepare-transfer.mdx @@ -0,0 +1,298 @@ +--- +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 `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 `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. + + + 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": { + "signingLeaf": { + "leafId": "" + } + }, + "newLeafDerivation": { + "signingLeaf": { + "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: { signingLeaf: { leafId: "" } }, + newLeafDerivation: { signingLeaf: { 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..1ae3f8fc --- /dev/null +++ b/api-reference/activities/spark-sign-frost.mdx @@ -0,0 +1,302 @@ +--- +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).

+ + + A oneof descriptor — set exactly one of the following keys. The selected key tells the enclave which private key to use for this signature. + + | 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 | + + + + 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. + + + + + Empty object `{}`. Set this key to sign with the single-use deposit key derived at deposit time. + + + 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. + + + + + 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": { + "signingLeaf": { + "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: { + signingLeaf: { 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..17d467dc --- /dev/null +++ b/networks/spark-flows.mdx @@ -0,0 +1,567 @@ +--- +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. 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). + +--- + +## 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. + +- **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. + +--- + +## 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. 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 + +> **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 + +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 + +> **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 + +Call [`SPARK_SIGN_FROST`](/api-reference/activities/spark-sign-frost) with a batch of 2 signatures: + +```json +{ + "signWith": "spark1pgss...", + "signatures": [ + { + "derivation": { "deposit": {} }, + "message": "", + "operatorCommitments": [{ "id": "SO1", "hiding": "", "binding": "" }], + "verifyingKey": "" + }, + { + "derivation": { "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). + +> **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. + +### 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 + +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. + +### Step 2 — Get SO signing commitments + +> **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 + +Call [`SPARK_SIGN_FROST`](/api-reference/activities/spark-sign-frost): + +```json +{ + "signWith": "spark1pgss...", + "signatures": [{ + "derivation": { "signingLeaf": { "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. The pre-signed exit transactions stored at deposit time are now spent and can be discarded — the UTXO no longer exists on L1. + +--- + +## 3. Unilateral Exit: Spark → Bitcoin L1 (Emergency) + +**Turnkey activities:** None + +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 + +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 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). + + +--- + +## 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 + +> **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 + +> **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 + +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": { "signingLeaf": { "leafId": "bob-new-leaf-id" } }, + "message": "", + "operatorCommitments": [...], + "verifyingKey": "" + }, + { + "derivation": { "signingLeaf": { "leafId": "bob-new-leaf-id" } }, + "message": "", + "operatorCommitments": [...], + "verifyingKey": "" + }, + { + "derivation": { "signingLeaf": { "leafId": "bob-new-leaf-id" } }, + "message": "", + "operatorCommitments": [...], + "verifyingKey": "" + }, + { + "derivation": { "signingLeaf": { "leafId": "alice-change-leaf-id" } }, + "message": "", + "operatorCommitments": [...], + "verifyingKey": "" + }, + { + "derivation": { "signingLeaf": { "leafId": "alice-change-leaf-id" } }, + "message": "", + "operatorCommitments": [...], + "verifyingKey": "" + }, + { + "derivation": { "signingLeaf": { "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": { "signingLeaf": { "leafId": "old-leaf-id" } }, + "newLeafDerivation": { "signingLeaf": { "leafId": "bob-new-leaf-id" } }, + "refundSignature": "", + "directRefundSignature": "", + "directFromCpfpRefundSignature": "" + }, + { + "leafId": "old-leaf-id", + "oldLeafDerivation": { "signingLeaf": { "leafId": "old-leaf-id" } }, + "newLeafDerivation": { "signingLeaf": { "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 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 + +Returns `operatorPackages[]` and `transferUserSignature`. + +### Step 5 — Submit to Spark Operators + +> **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). + +--- + +## 5. Claim Transfer: Spark → Spark (Receiver) + +**Turnkey activities:** `SPARK_CLAIM_TRANSFER` ×1 + +### Step 1 — Poll SOs for pending transfers + +> **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 + +Call [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/spark-claim-transfer): + +```json +{ + "signWith": "spark1pbob...", + "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 — Store new leaf in local wallet state + +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 — Submit claim packages to SOs + +> **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). + +--- + +## 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 + +> **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 + +Construct a BOLT11 invoice encoding `paymentHash`. Share this invoice with the payer. + +### Step 4 — Lock leaves to the SSP + +> **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 + +> **SSP call:** The SSP routes the payment across the Lightning Network. + +### Step 6 — SOs reveal preimage to 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 + +> **SSP call:** The SSP reveals the preimage to the payer's node. The Lightning payment settles. + +### Step 8 — SOs transfer leaves to the SSP + +> **SO call:** 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, `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. + +### 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. + +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: + +```json +{ + "address": "", + "targetPublicKey": "" +} +``` + +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 +> - 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 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 5 — Construct branch and exit transactions + +Client-side: build unsigned `Txn1` (branch) and `Txn2` (exit, timelocked) using the aggregate public key. + +### 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 7 — FROST sign branch and exit transactions + +Call [`SPARK_SIGN_FROST`](/api-reference/activities/spark-sign-frost) using the `staticDeposit` derivation key and the index: + +```json +{ + "signWith": "spark1pgss...", + "signatures": [ + { + "derivation": { "staticDeposit": { "index": 0 } }, + "message": "", + "operatorCommitments": [...], + "verifyingKey": "" + }, + { + "derivation": { "staticDeposit": { "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 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 9 — Broadcast deposit transaction + +Broadcast a standard Bitcoin transaction sending BTC to the static `bc1p...` Taproot address. + +### Step 10 — Wait for L1 confirmation + +The SOs register a new Spark leaf for each confirmed deposit to this address. + +--- + +## 8. Static Deposit Claim + +**Turnkey activities:** `SPARK_CLAIM_TRANSFER` ×1 + +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 + +After a deposit to the static address confirms on-chain, the SOs register a new leaf. + +> **SSP call:** Fetch a fee quote for the claim: `wallet.getClaimStaticDepositQuote(txid, outputIndex)`. + +### 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 +{ + "signWith": "spark1p...", + "claim": { + "transferId": "", + "senderIdentityPublicKey": "", + "threshold": 2, + "operatorRecipients": [ + { "operatorId": "SO1", "encryptionPublicKey": "02abc..." }, + { "operatorId": "SO2", "encryptionPublicKey": "02def..." } + ], + "leaves": [{ + "leafId": "", + "ciphertext": "", + "senderSignature": "" + }] + } +} +``` + +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 + +Returns `operatorPackages[]`. + +### Step 3 — Submit claim packages to SOs + +> **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 + +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!