Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions docs/smart-accounts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,13 +230,10 @@ val lowLevelResult = kit.signerManager.addPasskey(
credentialId = otherCredentialId // raw credential ID bytes
)

// Remove a signer
val delegatedSigner = DelegatedSigner(
address = "GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ"
)
// Remove a signer by its on-chain signer ID
val removeResult = kit.signerManager.removeSigner(
contextRuleId = 0u,
signer = delegatedSigner
signerId = 1u
)
```

Expand Down
96 changes: 87 additions & 9 deletions docs/smart-accounts/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,8 @@ if (result.success) {
suspend fun submit(
hostFunction: HostFunctionXdr,
auth: List<SorobanAuthorizationEntryXdr>,
forceMethod: SubmissionMethod? = null
forceMethod: SubmissionMethod? = null,
resolveContextRuleIds: ResolveContextRuleIds? = null
): TransactionResult
```

Expand All @@ -566,13 +567,63 @@ Handles simulation, auth entry extraction, WebAuthn signing, re-simulation, and
- `hostFunction`: The Soroban host function to execute
- `auth`: Initial authorization entries (typically empty; simulation provides them)
- `forceMethod`: Optional submission method override
- `resolveContextRuleIds`: Optional callback that returns context rule IDs for each authorization entry. Called once per entry with the entry and its index. When provided, the contract evaluates only the returned rules instead of scanning all matching rules. See [ResolveContextRuleIds](#resolvecontextruleids).

**Returns**: `TransactionResult` with submission outcome

**Throws**: Multiple exception types (see transaction operations exceptions)

---

#### executeAndSubmit

```kotlin
suspend fun executeAndSubmit(
target: String,
targetFn: String,
targetArgs: List<SCValXdr> = emptyList(),
forceMethod: SubmissionMethod? = null,
resolveContextRuleIds: ResolveContextRuleIds? = null
): TransactionResult
```

Executes a generic contract call through the smart account's `execute` entry point. Builds the invocation, handles simulation, WebAuthn signing, and submission in one step. Designed for single-signer workflows where the connected passkey authorizes the call.

**Parameters**:
- `target`: Contract address to call (C-address)
- `targetFn`: Function name to invoke on the target contract
- `targetArgs`: Arguments for the target function as XDR values
- `forceMethod`: Optional submission method override
- `resolveContextRuleIds`: Optional callback that returns context rule IDs for each authorization entry. See [ResolveContextRuleIds](#resolvecontextruleids).

**Returns**: `TransactionResult` with submission outcome

**Throws**:
- `WalletException.NotConnected`: Wallet is not connected
- `ValidationException`: Invalid addresses or arguments
- `TransactionException`: Simulation, signing, or submission failed
- `WebAuthnException`: Biometric authentication failed

**Example**:

```kotlin
// Call a custom contract function through the smart account
val result = kit.transactionOperations.executeAndSubmit(
target = "CBCD1234...",
targetFn = "approve",
targetArgs = listOf(
Scv.toAddress("GA7QYNF7..."),
Scv.toInt128(1_000_000_000L)
)
)

if (result.success) {
println("Executed: ${result.hash}")
}
```

---

#### fundWallet

```kotlin
Expand Down Expand Up @@ -908,17 +959,17 @@ Adds an Ed25519 signer to a context rule.
```kotlin
suspend fun removeSigner(
contextRuleId: UInt,
signer: SmartAccountSigner
signerId: UInt
): TransactionResult
```

Removes a signer from a context rule.
Removes a signer from a context rule by its on-chain signer ID.

**Note**: Cannot remove the last signer unless policies exist.

**Parameters**:
- `contextRuleId`: Context rule ID
- `signer`: The signer to remove
- `signerId`: The on-chain ID of the signer to remove

**Returns**: `TransactionResult`

Expand Down Expand Up @@ -1070,15 +1121,15 @@ val result = kit.policyManager.addSpendingLimit(
```kotlin
suspend fun removePolicy(
contextRuleId: UInt,
policyAddress: String
policyId: UInt
): TransactionResult
```

Removes a policy from a context rule.
Removes a policy from a context rule by its on-chain policy ID.

**Parameters**:
- `contextRuleId`: Context rule ID
- `policyAddress`: Policy contract address to remove
- `policyId`: The on-chain ID of the policy to remove

**Returns**: `TransactionResult`

Expand Down Expand Up @@ -1265,7 +1316,8 @@ suspend fun multiSignerTransfer(
tokenContract: String,
recipient: String,
amount: String,
selectedSigners: List<SelectedSigner>
selectedSigners: List<SelectedSigner>,
resolveContextRuleIds: ResolveContextRuleIds? = null
): TransactionResult
```

Expand All @@ -1278,6 +1330,7 @@ The caller explicitly lists every signer. There is no implicit connected passkey
- `recipient`: Recipient address (G-address or C-address)
- `amount`: Amount in XLM
- `selectedSigners`: All signers that must sign, in collection order
- `resolveContextRuleIds`: Optional callback that returns context rule IDs for each authorization entry. See [ResolveContextRuleIds](#resolvecontextruleids).

**Returns**: `TransactionResult`

Expand Down Expand Up @@ -1818,6 +1871,31 @@ Represents a WebAuthn signature with authenticator and client data.

---

### ResolveContextRuleIds

```kotlin
typealias ResolveContextRuleIds = suspend (
entry: SorobanAuthorizationEntryXdr,
index: Int
) -> List<UInt>
```

Callback that resolves context rule IDs for a given authorization entry during transaction signing. Called once per entry. The `entry` is the authorization entry being signed, and `index` is its position in the authorization list. Return the list of context rule IDs the contract should evaluate for that entry.

**Usage**:

```kotlin
// Same rule for all entries
resolveContextRuleIds = { _, _ -> listOf(ruleId) }

// Different rules per entry
resolveContextRuleIds = { entry, index ->
if (index == 0) listOf(1u) else listOf(2u)
}
```

---

### SubmissionMethod

```kotlin
Expand Down Expand Up @@ -1931,4 +2009,4 @@ Stellar SDK Kotlin Multiplatform - Apache License 2.0

---

**Last Updated**: February 2026
**Last Updated**: April 2026
Original file line number Diff line number Diff line change
Expand Up @@ -265,13 +265,13 @@ struct KnownSignersScreen: View {
} else if let external = signer as? ExternalSigner {
let keyData = external.keyData
// Determine passkey vs Ed25519 by key length:
// WebAuthn compressed P-256 keys are 65 bytes; Ed25519 keys are 32 bytes.
if keyData.size == 32 {
// WebAuthn P-256 keys are 65 bytes (+ credential ID suffix); Ed25519 keys are 32 bytes.
if keyData.size <= 32 {
signerType = "ed25519"
identifier = KotlinInterop.hexString(from: keyData)
} else {
signerType = "passkey"
identifier = KotlinInterop.hexString(from: keyData)
identifier = bridgeWrapper.bridge.getCredentialIdFromSigner(signer: external) ?? KotlinInterop.hexString(from: keyData)
}
} else {
signerType = "ed25519"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,19 @@ object DemoConfig {

// -- Smart Account Contract --

/** WASM hash of the multisig smart account contract (OZ stellar-contracts v0.6.0).
/** WASM hash of the multisig smart account contract (OZ stellar-contracts v0.7.0).
* Passed to OZSmartAccountConfig.accountWasmHash for wallet deployment.
* This hash can change when the contract is upgraded or testnet is reset.
* See docs/smart-accounts/README.md#testnet-contract-addresses for upload instructions. */
const val ACCOUNT_WASM_HASH = "64086253db59176c3bbbcf57fbb68c0a2fbe6fe9e0b05883ff1da44c5978ae4c"
const val ACCOUNT_WASM_HASH = "3e51f5b222dec74650f0b33367acb42a41ce497f72639230463070e666abba2c"

// -- Verifier Contracts --

/** WebAuthn (secp256r1) signature verifier contract. Validates passkey signatures on-chain. */
const val WEBAUTHN_VERIFIER_ADDRESS = "CBSHV66WG7UV6FQVUTB67P3DZUEJ2KJ5X6JKQH5MFRAAFNFJUAJVXJYV"
const val WEBAUTHN_VERIFIER_ADDRESS = "CATPTBRWVMH5ZCIKO5HN2F4FMPXVZEXC56RKGHRXCM7EEZGGXK7PICEH"

/** Ed25519 signature verifier contract. Validates Ed25519 signer signatures on-chain. */
const val ED25519_VERIFIER_ADDRESS = "CDGMOL3BP6Y6LYOXXTRNXBNJ2SLNTQ47BGG3LOS2OBBE657E3NYCN54B"
const val ED25519_VERIFIER_ADDRESS = "CAIKK32K3BZJYTWVTXHZFPIEEDBR6YCVTGPABH4UQUQ4XFA3OLYXG27G"

// -- Token Contracts --

Expand Down Expand Up @@ -110,13 +110,13 @@ val KNOWN_POLICIES = listOf(
type = "threshold",
name = "Threshold (M-of-N)",
description = "Requires M signatures out of N total signers",
address = "CCT4MMN5MJ6O2OU6LXPYTCVORQ2QVTBMDJ7MYBZQ2ULSYQVUIYP4IFYD"
address = "CDDQLFG7CV74QHWPSP6NZIPNBR2PPCMTUVYCJF4P3ONDYHODRFGR7LWC"
),
PolicyInfo(
type = "spending_limit",
name = "Spending Limit",
description = "Limits spending to a maximum amount per time period",
address = "CBMMWY54XOV6JJHSWCMKWWPXVRXASR5U26UJMLZDN4SP6CFFTVZARPTY"
address = "CBYLPYZGLQ6JVY2IQ5P23QLQPR3KAMMKMZLNWG6RUUKJDNYGPLVHK7U4"
),
PolicyInfo(
type = "weighted_threshold",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package com.soneso.smartdemo.flows
* Business logic for managing context rules on a smart account.
*
* Functions provided by this file:
* - Loading rules: [loadContextRules], [loadContextRule]
* - Loading rules: [loadContextRules], [loadParsedContextRule]
* - Modifying rules: [addContextRule], [removeContextRule], [updateContextRuleName], [updateContextRuleValidUntil]
* - Signer construction: [registerPasskeySigner], [buildDelegatedSigner], [buildEd25519Signer]
* - Helpers: [resolveAbsoluteLedger], [loadAvailablePasskeySigners]
Expand All @@ -13,9 +13,9 @@ package com.soneso.smartdemo.flows
* authorize which operations (Default, CallContract, or CreateContract) and which
* policy contracts are enforced. All modifying operations require passkey authentication.
*
* Rule data is stored on-chain as Soroban SCVal maps and parsed by [fetchAllContextRules]
* from ContextRuleParser.kt. Individual rule parsing is done via parseSingleContextRuleFromScVal
* (defined in ContextRuleParser.kt) by the calling screen.
* Rule data is fetched via [OZContextRuleManager.listContextRules] which returns fully
* parsed [ParsedContextRule] objects including the [ParsedContextRule.signerIds] and
* [ParsedContextRule.policyIds] fields introduced in v0.7.0.
*/

import com.soneso.smartdemo.config.DemoConfig
Expand Down Expand Up @@ -62,43 +62,37 @@ data class FlowPolicyEntry(
/**
* Loads all context rules from the connected smart account.
*
* SDK workflow:
* 1. Call [contextRuleManager.getContextRulesCount] to get the total rule count.
* 2. Iterate IDs from 0 upward, calling [contextRuleManager.getContextRule] for each.
* Gaps from removed rules are skipped.
* 3. Parse each SCVal result using [parseSingleContextRuleFromScVal] from ContextRuleParser.
* Delegates to [fetchAllContextRules] which calls [OZContextRuleManager.listContextRules].
* The returned rules include [ParsedContextRule.signerIds] and [ParsedContextRule.policyIds]
* populated by the SDK.
*
* @return List of [ParsedContextRule], sorted by ID with duplicates removed.
* @return List of [ParsedContextRule], sorted by ID.
* @throws IllegalStateException if the kit is not initialized.
*/
suspend fun loadContextRules(): List<ParsedContextRule> {
// fetchAllContextRules reads from DemoState.kit internally and handles the
// count-then-fetch pattern with a fallback for contracts without count support.
val rules = fetchAllContextRules()
ActivityLogState.info("Fetched ${rules.size} context rule(s)")
return rules
}

/**
* Loads a single context rule by ID for edit mode pre-population.
* Loads a single context rule by ID as a fully parsed [ParsedContextRule].
*
* SDK workflow:
* - Calls [OZSmartAccountKit.contextRuleManager.getContextRule] which returns the
* rule's on-chain data as a raw SCVal map.
* - The screen parses the result using [parseSingleContextRuleFromScVal] to populate
* its form fields (name, context type, signers, policies, expiry).
* Fetches all context rules via [OZContextRuleManager.listContextRules] and returns the
* rule matching [ruleId]. This ensures the result includes [ParsedContextRule.signerIds]
* and [ParsedContextRule.policyIds] as populated by the SDK parser.
*
* @param ruleId The rule ID (0-indexed) to load.
* @return The raw [SCValXdr] for parsing by the caller.
* @throws Exception if the rule does not exist or the RPC call fails.
* @param ruleId The rule ID to load.
* @return The parsed [ParsedContextRule].
* @throws IllegalStateException if the kit is not initialized.
* @throws NoSuchElementException if no rule with the given ID exists.
*/
suspend fun loadContextRule(ruleId: UInt): SCValXdr {
suspend fun loadParsedContextRule(ruleId: UInt): ParsedContextRule {
val kit = DemoState.kit
?: throw IllegalStateException("Kit not initialized")

// getContextRule returns the raw on-chain SCVal so the screen can parse
// it into typed form fields using parseSingleContextRuleFromScVal.
return kit.contextRuleManager.getContextRule(ruleId)
return kit.contextRuleManager.listContextRules()
.firstOrNull { it.id == ruleId }
?: throw NoSuchElementException("Context rule #$ruleId not found")
}

/**
Expand Down
Loading
Loading