passkey-kit 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/PROPOSAL.md ADDED
@@ -0,0 +1,397 @@
1
+ # WebAuthn smart wallet contract interface
2
+
3
+ With the release of [Protocol 21](https://stellar.org/blog/developers/announcing-protocol-21) (and specifically the inclusion of the secp256r1 verification curve) Soroban now has tremendous first class support for passkey powered smart wallets.
4
+
5
+ Over the past months I've been hard at work designing a solid first stab at a v1 smart wallet contract interface for mainnet use. This is the culmination of that work in proposal form.
6
+
7
+ All the best work can reviewed in my [passkey-kit](https://github.com/kalepail/passkey-kit) repo. This repo includes the factory and wallet contracts, a demo client interface, a `passkey-kit` SDK tool to make interacting with the contract interface simple and painless and finally a [Mercury Zephyr](https://www.mercurydata.app/products/zephyr-vm) program for indexing contract events in order to make the wallet more usable client side.
8
+
9
+ This repo also makes use of a new [Launchtube service](https://github.com/kalepail/launchtube) which makes submitting Soroban transactions simple by handling the concerns of both transaction fees and sequence numbers.
10
+
11
+ The primary interest of this proposal is to detail the contract interface itself but many of the design decisions are informed by complexities and available solutions external to the interface. A well rounded understandings of all that's involved to make passkey powered smart wallets on Stellar actually work is necessary in order to arrive at a truly viable contract interface.
12
+
13
+ This proposal consists of two contracts, a factory “deployer” contract and the actual smart wallet interface.
14
+
15
+ # Contract 1: The Factory
16
+
17
+ Stellar doesn’t allow us to both deploy and initialize a contract atomically and so the ecosystem has adopted the workaround of having a factory contract which handles the deploying and then calling of the newly deployed contract’s initialize function. This deploy and init can happen atomically within Soroban.
18
+
19
+ The side benefit is we can ensure consistency of all contracts deployed from the same factory address. As long as the contract was deployed from a known factory address users and services have a guarantee of the initial inner form of the smart wallet. As we’ll see smart wallets have an `upgrade` method which will effectively break this guarantee but at the end of the day it’s a contract’s WASM hash we actually care about vs it’s factory address.
20
+
21
+ ## Interface
22
+
23
+ ```rust
24
+ // FUNCTIONS
25
+
26
+ fn init(wasm_hash: bytesn<32>) -> result<tuple<>,error>
27
+
28
+ fn deploy(id: bytes, pk: bytesn<65>) -> result<address,error>
29
+
30
+ // ERRORS
31
+
32
+ #[contracterror]
33
+ enum Error {
34
+ NotInitialized = 1,
35
+ AlreadyInitialized = 2
36
+ }
37
+ ```
38
+
39
+ ## Code
40
+
41
+ [https://github.com/kalepail/passkey-kit/blob/main/contracts/contract-webauthn-factory/src/lib.rs](https://github.com/kalepail/passkey-kit/blob/main/contracts/contract-webauthn-factory/src/lib.rs)
42
+
43
+
44
+
45
+ ```rust
46
+ const WEEK_OF_LEDGERS: u32 = 60 * 60 * 24 / 5 * 7;
47
+ const STORAGE_KEY_WASM_HASH: Symbol = symbol_short!("hash");
48
+ ```
49
+
50
+ Only thing to note in this block is I’m opting to max extend this contract’s instance during every call with a threshold of 7 days. This will be the same for the wallet interface itself. This will cause the initial calls for any storage write function to be somewhat inflated with the beneficial tradeoff that folks won’t have to worry about their wallets or keys expiring or archiving for `max_ttl` time. In my tests this cost was minimal and the improved UX of not having to worry about restoring archived entries in my opinion was worth it.
51
+
52
+ We could decide to make these values instance variables which could be updated or even make them configurable on a key by key basis however that would increase complexity and cost in many cases and without further real world data to support that choice I’m suggesting simplicity.
53
+
54
+ ### `init`
55
+ ```rust
56
+ pub fn init(env: Env, wasm_hash: BytesN<32>) -> Result<(), Error> {
57
+ if env.storage().instance().has(&STORAGE_KEY_WASM_HASH) {
58
+ return Err(Error::AlreadyInitialized);
59
+ }
60
+
61
+ let max_ttl = env.storage().max_ttl();
62
+
63
+ env.storage()
64
+ .instance()
65
+ .set(&STORAGE_KEY_WASM_HASH, &wasm_hash);
66
+
67
+ env.storage()
68
+ .instance()
69
+ .extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl);
70
+
71
+ Ok(())
72
+ }
73
+ ```
74
+
75
+ Nothing controversial here I don’t think. We’re storing the smart wallet’s wasm hash in order to load up the factory with the proper template to deploy in the `deploy` function. This is stored on the instance as it should be and then the instance is extended
76
+
77
+ ### `deploy`
78
+ ```rust
79
+ pub fn deploy(env: Env, salt: BytesN<32>, id: Bytes, pk: BytesN<65>) -> Result<Address, Error> {
80
+ let wasm_hash = env
81
+ .storage()
82
+ .instance()
83
+ .get::<Symbol, BytesN<32>>(&STORAGE_KEY_WASM_HASH)
84
+ .ok_or(Error::NotInitialized)?;
85
+
86
+ let address = env
87
+ .deployer()
88
+ .with_current_contract(salt)
89
+ .deploy(wasm_hash);
90
+
91
+ let () = env.invoke_contract(
92
+ &address,
93
+ &symbol_short!("add"),
94
+ vec![&env, id.to_val(), pk.to_val(), true.into()],
95
+ );
96
+
97
+ let max_ttl = env.storage().max_ttl();
98
+
99
+ env.storage()
100
+ .instance()
101
+ .extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl);
102
+
103
+ Ok(address)
104
+ }
105
+ ```
106
+
107
+ Few things to note here:
108
+
109
+ - Also note we’re calling the `env.invoke_contract` vs pulling in the smart wallet interface. This is a cost savings as we’re only making use of the `add` method. This requires knowing intuitively how to properly construct the invocation but let’s be honest, that’s not hard.
110
+ - Last thing is extending the interface again. If you’re gonna use the factory at least pay it forward a little to help keep the factory’s lights on.
111
+
112
+ # Contract 2: The Smart Wallet
113
+
114
+ The smart wallet interface while obviously more complex than the factory is still aiming to be as simple as possible and only do what’s absolutely necessary to provide a useful smart wallet interface for general purpose usage.
115
+
116
+ I’ve intentionally left off as many bells and whistles as possible with the hope of being able to agree and progress with this interface into an audited and approved mainnet interface for general usage. Certainly there will be additional features and functions users and services will want and I hope to see a rich and diverse ecosystem of wallet interfaces arise over time but initially we just need to get something sufficiently useful live providing the basic majority needs of non crypto-native users.
117
+
118
+ ## Interface
119
+
120
+ ```rust
121
+ // FUNCTIONS
122
+
123
+ fn add(id: bytes, pk: bytesn<65>, admin: bool) -> result<tuple<>,error>
124
+
125
+ fn remove(id: bytes) -> result<tuple<>,error>
126
+
127
+ fn upgrade(hash: bytesn<32>) -> result<tuple<>,error>
128
+
129
+ fn __check_auth(signature_payload: bytesn<32>, signature: Signature, auth_contexts: vec<Context>) -> result<tuple<>,error>
130
+
131
+ // STRUCTS
132
+
133
+ #[contracttype]
134
+ struct Signature {
135
+ authenticator_data: bytes,
136
+ client_data_json: bytes,
137
+ id: bytes,
138
+ signature: bytesn<64>
139
+ }
140
+
141
+ // ERRORS
142
+
143
+ #[contracterror]
144
+ enum Error {
145
+ NotFound = 1,
146
+ NotPermitted = 2,
147
+ ClientDataJsonChallengeIncorrect = 3,
148
+ Secp256r1PublicKeyParse = 4,
149
+ Secp256r1SignatureParse = 5,
150
+ Secp256r1VerifyFailed = 6,
151
+ JsonParseError = 7,
152
+ }
153
+
154
+ ```
155
+
156
+ ## Code
157
+
158
+ [https://github.com/kalepail/passkey-kit/blob/main/contracts/contract-webauthn-secp256r1/src/lib.rs](https://github.com/kalepail/passkey-kit/blob/main/contracts/contract-webauthn-secp256r1/src/lib.rs)
159
+
160
+ ### `add`
161
+ ```rust
162
+ pub fn add(env: Env, id: Bytes, pk: BytesN<65>, mut admin: bool) -> Result<(), Error> {
163
+ if env.storage().instance().has(&ADMIN_SIGNER_COUNT) {
164
+ env.current_contract_address().require_auth();
165
+ } else {
166
+ admin = true;
167
+ }
168
+
169
+ let max_ttl = env.storage().max_ttl();
170
+
171
+ if admin {
172
+ if env.storage().temporary().has(&id) {
173
+ env.storage().temporary().remove(&id);
174
+ }
175
+
176
+ Self::update_admin_signer_count(&env, true);
177
+
178
+ env.storage().persistent().set(&id, &pk);
179
+
180
+ env.storage()
181
+ .persistent()
182
+ .extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl);
183
+ } else {
184
+ if env.storage().persistent().has(&id) {
185
+ Self::update_admin_signer_count(&env, false);
186
+
187
+ env.storage().persistent().remove(&id);
188
+ }
189
+
190
+ env.storage().temporary().set(&id, &pk);
191
+
192
+ env.storage()
193
+ .temporary()
194
+ .extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl);
195
+ }
196
+
197
+ env.storage()
198
+ .instance()
199
+ .extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl);
200
+
201
+ env.events()
202
+ .publish((EVENT_TAG, symbol_short!("add"), id), (pk, admin));
203
+
204
+ Ok(())
205
+ }
206
+ ```
207
+
208
+ Some notable elements:
209
+
210
+ - We use the `env.storage().instance().has(&ADMIN_SIGNER_COUNT)` to toggle between a sort of initialization call and the standard `require_auth` flow.
211
+
212
+ ```rust
213
+ if env.storage().instance().has(&ADMIN_SIGNER_COUNT) {
214
+ env.current_contract_address().require_auth();
215
+ } else {
216
+ admin = true;
217
+ }
218
+ ```
219
+
220
+ The only potential downside is `add` includes logic for storing temporary session signers which an initial call doesn't support making that logic verbose. Initially I had a separate `init` function but I think this is a better tradeoff for simplicity and efficiency even if there are some unusable if statements in the case of the initial `add` call made by the factory contract.
221
+
222
+ - `Self::update_admin_signer_count(&env, true);` My proposal includes the concept of session and admin signers. Certain functions, well really all of the smart wallet self functions (`add`, `remove`, `upgrade`) are only callable by admin signers. Given this we need to ensure we never remove all the admin signers which necessarily requires we track the number of admin signers. This function provides that service and will be called anytime we add or remove an admin signer.
223
+ - Admin signers are persistent entries, non-admin signers are temporary. It’s also possible for signers to be toggled between admin and non however we must only ever be tracking a single `id` to a single `pk` and so we must add logic for removing any existing signers for a given `id` in the counter storage to the type we’re currently adding to. Make special note of the need to decrement the `ADMIN_SIGNER_COUNT` in case of removing an admin signer to temporary if a persistent entry for that `id` exists.
224
+
225
+ ```rust
226
+ if admin {
227
+ if env.storage().temporary().has(&id) {
228
+ env.storage().temporary().remove(&id);
229
+ }
230
+
231
+ Self::update_admin_signer_count(&env, true);
232
+
233
+ env.storage().persistent().set(&id, &pk);
234
+
235
+ env.storage()
236
+ .persistent()
237
+ .extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl);
238
+ } else {
239
+ if env.storage().persistent().has(&id) {
240
+ Self::update_admin_signer_count(&env, false);
241
+
242
+ env.storage().persistent().remove(&id);
243
+ }
244
+
245
+ env.storage().temporary().set(&id, &pk);
246
+
247
+ env.storage()
248
+ .temporary()
249
+ .extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl);
250
+ }
251
+ ```
252
+
253
+ ### `remove`
254
+ ```rust
255
+ pub fn remove(env: Env, id: Bytes) -> Result<(), Error> {
256
+ env.current_contract_address().require_auth();
257
+
258
+ if env.storage().temporary().has(&id) {
259
+ env.storage().temporary().remove(&id);
260
+ } else {
261
+ Self::update_admin_signer_count(&env, false);
262
+
263
+ env.storage().persistent().remove(&id);
264
+ }
265
+
266
+ let max_ttl = env.storage().max_ttl();
267
+
268
+ env.storage()
269
+ .instance()
270
+ .extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl);
271
+
272
+ env.events()
273
+ .publish((EVENT_TAG, symbol_short!("remove"), id), ());
274
+
275
+ Ok(())
276
+ }
277
+ ```
278
+
279
+ Remove is similar to `add` just in inverse with some slight simplifications.
280
+
281
+ - Given the key could be either temporary or persistent we must include logic for checking both and removing if they exist. Again note the need to decrement the `ADMIN_SIGNER_COUNT` in case of a persistent admin `id`.
282
+ - Given each `id` can only be either a temporary or persistent entry it's safe to use `else if env.storage().persistent().has(&id)` vs a separate `if ...`. Doing so saves on some read costs if we were to try and just remove both storage type for the same `id` key. Note we do need to use the has check vs just doing an `else` check as a `storage.remove` won't error if the entry doesn't exist which would open us up to the issue of decrementing the admin key count when we didn't actually delete anything.
283
+
284
+ ### `update`
285
+ ```rust
286
+ pub fn update(env: Env, hash: BytesN<32>) -> Result<(), Error> {
287
+ env.current_contract_address().require_auth();
288
+
289
+ env.deployer().update_current_contract_wasm(hash);
290
+
291
+ let max_ttl = env.storage().max_ttl();
292
+
293
+ env.storage()
294
+ .instance()
295
+ .extend_ttl(max_ttl - WEEK_OF_LEDGERS, max_ttl);
296
+
297
+ Ok(())
298
+ }
299
+ ```
300
+
301
+ An essential function for all smart wallets imo. The ability to change the interface the wallet implements. Perhaps controversial given the risk of upgrading to a bugged or malicious wallet interface but that’s an risk inherent to creating a smart wallet in the first place and given that risk I actually think part of mitigating that risk is allowing users to move their interface to alternatives should they choose to. Client interfaces should be very careful in exposing this method to wallet users but I do think it’s an essential method for the health and safety of the smart wallet ecosystem.
302
+
303
+ - Protected such that only admin signers can perform this method.
304
+ - Allows for a wallet user to switch or update their interface should newer or different interfaces be released.
305
+
306
+ ### `__check_auth`
307
+ ```rust
308
+ fn __check_auth(
309
+ env: Env,
310
+ signature_payload: Hash<32>,
311
+ signature: Signature,
312
+ auth_contexts: Vec<Context>,
313
+ ) -> Result<(), Error> {...}
314
+ ```
315
+
316
+ This is the beefy boy and most of it is only interesting to auditors ensuring the actual decoding and cryptography bits work as intended. I’ll detail the parts here which are more specific to the interface itself:
317
+
318
+ - We need to select which `pk` to use for the provided `id` purporting to have signed for the incoming payload.
319
+
320
+ ```rust
321
+ let pk = match env.storage().temporary().get(&id) {
322
+ Some(pk) => {
323
+ ...
324
+
325
+ env.storage()
326
+ .temporary()
327
+ .extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl);
328
+
329
+ pk
330
+ }
331
+ None => {
332
+ env.storage()
333
+ .persistent()
334
+ .extend_ttl(&id, max_ttl - WEEK_OF_LEDGERS, max_ttl);
335
+
336
+ env.storage().persistent().get(&id).ok_or(Error::NotFound)?
337
+ }
338
+ };
339
+ ```
340
+
341
+ We do that first by looking up the temporary entry which will be the far more common case. If we cannot find it there we look for a persistent entry. This will introduce a double look up for a temporary entry but those are cheap so this is fine. Note we also set the `admin` binary toggle for use later in blocking protected self methods.
342
+
343
+ - If the pk is a temporary session signer we need to do an additional check to ensure the authentication request isn’t for a protected action
344
+
345
+ ```rust
346
+
347
+ ...
348
+
349
+ for context in auth_contexts.iter() {
350
+ match context {
351
+ Context::Contract(c) => {
352
+ if c.contract == env.current_contract_address()
353
+ && (
354
+ c.fn_name != symbol_short!("remove")
355
+ || (
356
+ c.fn_name == symbol_short!("remove")
357
+ && Bytes::from_val(&env, &c.args.get(0).unwrap()) != id
358
+ )
359
+ )
360
+ {
361
+ return Err(Error::NotPermitted);
362
+ }
363
+ }
364
+ _ => {}
365
+ };
366
+ }
367
+
368
+ ...
369
+ ```
370
+
371
+ This is a relatively straight forward check. If the request is for the smart wallet contract ensure the only function it *might* be able to call is a `remove` of it’s own `id`. Anything else should result in an error.
372
+
373
+
374
+ The rest of `__check_auth` is boilerplate authentication checks of the webauthn data itself and not technically part of this interface. It needs to be audited but that won’t affect the final interface of the wallet.
375
+
376
+ # Events
377
+
378
+ The only other item worth mentioning are the events emitted during the `add` and `remove` methods. Events are emitted in order to allow an indexer to keep track of a wallet’s available signers and their current state as `admin` or not.
379
+
380
+ ## Add
381
+
382
+ ```rust
383
+ env.events().publish((EVENT_TAG, symbol_short!("add"), id), (pk, admin));
384
+ ```
385
+
386
+ - The `EVENT_TAG` is a trigger to help indexers only listen for relevant smart wallet events and while not fool proof should improve filtering out only those events which are relevant.
387
+ - The `pk` is emitted in order to allow downstream clients to queue up expired session signers to be re-added without needing to create new passkeys, you can continue to use the existing ones if you can find the `pk` for a matching `id` from a previously emitted event.
388
+
389
+ > [!CAUTION]
390
+ > Passkey public keys are only retrievable during a passkey creation flow. They cannot be later retrieved from an authentication flow. Thus passkey public keys are special data which we should be storing inside the blockchain itself. This is normally done during an `add` event but given we’re using temporary storage these keys could be lost and unrecoverable were we not to store them inside events for indexers to keep track of and then for clients to then be able to essentially “rehydrate” at a later date without requiring the user to keep creating new passkeys every time they wanted to sign into a service after their temporary session key had expired.
391
+
392
+
393
+ ## Remove
394
+
395
+ ```rust
396
+ env.events().publish((EVENT_TAG, symbol_short!("remove"), id), ());
397
+ ```
package/cheatsheet CHANGED
@@ -1,5 +1,5 @@
1
1
  # Install contract sdks
2
- npm publish --workspaces
2
+ npm publish --workspaces
3
3
 
4
4
  # Install passkey-kit
5
5
  pnpm publish --no-git-checks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "passkey-kit",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "A helper library for creating and using passkey accounts on the Stellar blockchain.",
5
5
  "author": "Tyler van der Hoeven",
6
6
  "license": "MIT",
@@ -13,8 +13,8 @@
13
13
  "base64url": "^3.0.1",
14
14
  "buffer": "^6.0.3",
15
15
  "cbor-x": "^1.5.9",
16
- "passkey-factory-sdk": "0.2.0",
17
- "passkey-kit-sdk": "0.2.0"
16
+ "passkey-factory-sdk": "0.2.1",
17
+ "passkey-kit-sdk": "0.2.1"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@simplewebauthn/types": "^10.0.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.2.0",
2
+ "version": "0.2.1",
3
3
  "name": "passkey-factory-sdk",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
@@ -15,7 +15,7 @@ if (typeof window !== 'undefined') {
15
15
  export const networks = {
16
16
  testnet: {
17
17
  networkPassphrase: "Test SDF Network ; September 2015",
18
- contractId: "CBCPNJNIR7I6ZI5AIXTMYI3MCDCSTNUZ57XL75HQWI6Y4ESWUP24HRBG",
18
+ contractId: "CCPLERXCJZB7LX2VOSOCBNRN754FRLHI6Y2AVOQBA5L7C2ZJX5RFVVET",
19
19
  }
20
20
  } as const
21
21
 
@@ -48,7 +48,7 @@ export interface Client {
48
48
  /**
49
49
  * Construct and simulate a deploy transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
50
50
  */
51
- deploy: ({ id, pk }: { id: Buffer, pk: Buffer }, options?: {
51
+ deploy: ({ salt, id, pk }: { salt: Buffer, id: Buffer, pk: Buffer }, options?: {
52
52
  /**
53
53
  * The fee to pay for the transaction. Default: BASE_FEE
54
54
  */
@@ -70,7 +70,7 @@ export class Client extends ContractClient {
70
70
  super(
71
71
  new ContractSpec(["AAAABAAAAAAAAAAAAAAABUVycm9yAAAAAAAAAgAAAAAAAAAOTm90SW5pdGlhbGl6ZWQAAAAAAAEAAAAAAAAAEkFscmVhZHlJbml0aWFsaXplZAAAAAAAAg==",
72
72
  "AAAAAAAAAAAAAAAEaW5pdAAAAAEAAAAAAAAACXdhc21faGFzaAAAAAAAA+4AAAAgAAAAAQAAA+kAAAPtAAAAAAAAAAM=",
73
- "AAAAAAAAAAAAAAAGZGVwbG95AAAAAAACAAAAAAAAAAJpZAAAAAAADgAAAAAAAAACcGsAAAAAA+4AAABBAAAAAQAAA+kAAAATAAAAAw=="]),
73
+ "AAAAAAAAAAAAAAAGZGVwbG95AAAAAAADAAAAAAAAAARzYWx0AAAD7gAAACAAAAAAAAAAAmlkAAAAAAAOAAAAAAAAAAJwawAAAAAD7gAAAEEAAAABAAAD6QAAABMAAAAD"]),
74
74
  options
75
75
  )
76
76
  }
@@ -3,7 +3,7 @@ import { AssembledTransaction, Client as ContractClient, ClientOptions as Contra
3
3
  export declare const networks: {
4
4
  readonly testnet: {
5
5
  readonly networkPassphrase: "Test SDF Network ; September 2015";
6
- readonly contractId: "CBCPNJNIR7I6ZI5AIXTMYI3MCDCSTNUZ57XL75HQWI6Y4ESWUP24HRBG";
6
+ readonly contractId: "CCPLERXCJZB7LX2VOSOCBNRN754FRLHI6Y2AVOQBA5L7C2ZJX5RFVVET";
7
7
  };
8
8
  };
9
9
  export declare const Errors: {
@@ -37,7 +37,8 @@ export interface Client {
37
37
  /**
38
38
  * Construct and simulate a deploy transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
39
39
  */
40
- deploy: ({ id, pk }: {
40
+ deploy: ({ salt, id, pk }: {
41
+ salt: Buffer;
41
42
  id: Buffer;
42
43
  pk: Buffer;
43
44
  }, options?: {
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.2.0",
2
+ "version": "0.2.1",
3
3
  "name": "passkey-kit-sdk",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
@@ -15,19 +15,18 @@ if (typeof window !== 'undefined') {
15
15
  export const networks = {
16
16
  testnet: {
17
17
  networkPassphrase: "Test SDF Network ; September 2015",
18
- contractId: "CBCPNJNIR7I6ZI5AIXTMYI3MCDCSTNUZ57XL75HQWI6Y4ESWUP24HRBG",
18
+ contractId: "CCPLERXCJZB7LX2VOSOCBNRN754FRLHI6Y2AVOQBA5L7C2ZJX5RFVVET",
19
19
  }
20
20
  } as const
21
21
 
22
22
  export const Errors = {
23
23
  1: { message: "NotFound" },
24
24
  2: { message: "NotPermitted" },
25
- 3: { message: "AlreadyInitialized" },
26
- 4: { message: "ClientDataJsonChallengeIncorrect" },
27
- 5: { message: "Secp256r1PublicKeyParse" },
28
- 6: { message: "Secp256r1SignatureParse" },
29
- 7: { message: "Secp256r1VerifyFailed" },
30
- 8: { message: "JsonParseError" }
25
+ 3: { message: "ClientDataJsonChallengeIncorrect" },
26
+ 4: { message: "Secp256r1PublicKeyParse" },
27
+ 5: { message: "Secp256r1SignatureParse" },
28
+ 6: { message: "Secp256r1VerifyFailed" },
29
+ 7: { message: "JsonParseError" }
31
30
  }
32
31
 
33
32
  export interface Signature {
@@ -38,26 +37,6 @@ export interface Signature {
38
37
  }
39
38
 
40
39
  export interface Client {
41
- /**
42
- * Construct and simulate a init transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
43
- */
44
- init: ({ id, pk }: { id: Buffer, pk: Buffer }, options?: {
45
- /**
46
- * The fee to pay for the transaction. Default: BASE_FEE
47
- */
48
- fee?: number;
49
-
50
- /**
51
- * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT
52
- */
53
- timeoutInSeconds?: number;
54
-
55
- /**
56
- * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true
57
- */
58
- simulate?: boolean;
59
- }) => Promise<AssembledTransaction<Result<void>>>
60
-
61
40
  /**
62
41
  * Construct and simulate a add transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
63
42
  */
@@ -99,9 +78,9 @@ export interface Client {
99
78
  }) => Promise<AssembledTransaction<Result<void>>>
100
79
 
101
80
  /**
102
- * Construct and simulate a upgrade transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
81
+ * Construct and simulate a update transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
103
82
  */
104
- upgrade: ({ hash }: { hash: Buffer }, options?: {
83
+ update: ({ hash }: { hash: Buffer }, options?: {
105
84
  /**
106
85
  * The fee to pay for the transaction. Default: BASE_FEE
107
86
  */
@@ -121,20 +100,18 @@ export interface Client {
121
100
  export class Client extends ContractClient {
122
101
  constructor(public readonly options: ContractClientOptions) {
123
102
  super(
124
- new ContractSpec(["AAAABAAAAAAAAAAAAAAABUVycm9yAAAAAAAACAAAAAAAAAAITm90Rm91bmQAAAABAAAAAAAAAAxOb3RQZXJtaXR0ZWQAAAACAAAAAAAAABJBbHJlYWR5SW5pdGlhbGl6ZWQAAAAAAAMAAAAAAAAAIENsaWVudERhdGFKc29uQ2hhbGxlbmdlSW5jb3JyZWN0AAAABAAAAAAAAAAXU2VjcDI1NnIxUHVibGljS2V5UGFyc2UAAAAABQAAAAAAAAAXU2VjcDI1NnIxU2lnbmF0dXJlUGFyc2UAAAAABgAAAAAAAAAVU2VjcDI1NnIxVmVyaWZ5RmFpbGVkAAAAAAAABwAAAAAAAAAOSnNvblBhcnNlRXJyb3IAAAAAAAg=",
125
- "AAAAAAAAAAAAAAAEaW5pdAAAAAIAAAAAAAAAAmlkAAAAAAAOAAAAAAAAAAJwawAAAAAD7gAAAEEAAAABAAAD6QAAA+0AAAAAAAAAAw==",
103
+ new ContractSpec(["AAAABAAAAAAAAAAAAAAABUVycm9yAAAAAAAABwAAAAAAAAAITm90Rm91bmQAAAABAAAAAAAAAAxOb3RQZXJtaXR0ZWQAAAACAAAAAAAAACBDbGllbnREYXRhSnNvbkNoYWxsZW5nZUluY29ycmVjdAAAAAMAAAAAAAAAF1NlY3AyNTZyMVB1YmxpY0tleVBhcnNlAAAAAAQAAAAAAAAAF1NlY3AyNTZyMVNpZ25hdHVyZVBhcnNlAAAAAAUAAAAAAAAAFVNlY3AyNTZyMVZlcmlmeUZhaWxlZAAAAAAAAAYAAAAAAAAADkpzb25QYXJzZUVycm9yAAAAAAAH",
126
104
  "AAAAAAAAAAAAAAADYWRkAAAAAAMAAAAAAAAAAmlkAAAAAAAOAAAAAAAAAAJwawAAAAAD7gAAAEEAAAAAAAAABWFkbWluAAAAAAAAAQAAAAEAAAPpAAAD7QAAAAAAAAAD",
127
105
  "AAAAAAAAAAAAAAAGcmVtb3ZlAAAAAAABAAAAAAAAAAJpZAAAAAAADgAAAAEAAAPpAAAD7QAAAAAAAAAD",
128
- "AAAAAAAAAAAAAAAHdXBncmFkZQAAAAABAAAAAAAAAARoYXNoAAAD7gAAACAAAAABAAAD6QAAA+0AAAAAAAAAAw==",
106
+ "AAAAAAAAAAAAAAAGdXBkYXRlAAAAAAABAAAAAAAAAARoYXNoAAAD7gAAACAAAAABAAAD6QAAA+0AAAAAAAAAAw==",
129
107
  "AAAAAQAAAAAAAAAAAAAACVNpZ25hdHVyZQAAAAAAAAQAAAAAAAAAEmF1dGhlbnRpY2F0b3JfZGF0YQAAAAAADgAAAAAAAAAQY2xpZW50X2RhdGFfanNvbgAAAA4AAAAAAAAAAmlkAAAAAAAOAAAAAAAAAAlzaWduYXR1cmUAAAAAAAPuAAAAQA==",
130
108
  "AAAAAAAAAAAAAAAMX19jaGVja19hdXRoAAAAAwAAAAAAAAARc2lnbmF0dXJlX3BheWxvYWQAAAAAAAPuAAAAIAAAAAAAAAAJc2lnbmF0dXJlAAAAAAAH0AAAAAlTaWduYXR1cmUAAAAAAAAAAAAADWF1dGhfY29udGV4dHMAAAAAAAPqAAAH0AAAAAdDb250ZXh0AAAAAAEAAAPpAAAD7QAAAAAAAAAD"]),
131
109
  options
132
110
  )
133
111
  }
134
112
  public readonly fromJSON = {
135
- init: this.txFromJSON<Result<void>>,
136
113
  add: this.txFromJSON<Result<void>>,
137
114
  remove: this.txFromJSON<Result<void>>,
138
- upgrade: this.txFromJSON<Result<void>>
115
+ update: this.txFromJSON<Result<void>>
139
116
  }
140
117
  }
@@ -3,7 +3,7 @@ import { AssembledTransaction, Client as ContractClient, ClientOptions as Contra
3
3
  export declare const networks: {
4
4
  readonly testnet: {
5
5
  readonly networkPassphrase: "Test SDF Network ; September 2015";
6
- readonly contractId: "CBCPNJNIR7I6ZI5AIXTMYI3MCDCSTNUZ57XL75HQWI6Y4ESWUP24HRBG";
6
+ readonly contractId: "CCPLERXCJZB7LX2VOSOCBNRN754FRLHI6Y2AVOQBA5L7C2ZJX5RFVVET";
7
7
  };
8
8
  };
9
9
  export declare const Errors: {
@@ -28,9 +28,6 @@ export declare const Errors: {
28
28
  7: {
29
29
  message: string;
30
30
  };
31
- 8: {
32
- message: string;
33
- };
34
31
  };
35
32
  export interface Signature {
36
33
  authenticator_data: Buffer;
@@ -39,26 +36,6 @@ export interface Signature {
39
36
  signature: Buffer;
40
37
  }
41
38
  export interface Client {
42
- /**
43
- * Construct and simulate a init transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
44
- */
45
- init: ({ id, pk }: {
46
- id: Buffer;
47
- pk: Buffer;
48
- }, options?: {
49
- /**
50
- * The fee to pay for the transaction. Default: BASE_FEE
51
- */
52
- fee?: number;
53
- /**
54
- * The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT
55
- */
56
- timeoutInSeconds?: number;
57
- /**
58
- * Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true
59
- */
60
- simulate?: boolean;
61
- }) => Promise<AssembledTransaction<Result<void>>>;
62
39
  /**
63
40
  * Construct and simulate a add transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
64
41
  */
@@ -100,9 +77,9 @@ export interface Client {
100
77
  simulate?: boolean;
101
78
  }) => Promise<AssembledTransaction<Result<void>>>;
102
79
  /**
103
- * Construct and simulate a upgrade transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
80
+ * Construct and simulate a update transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
104
81
  */
105
- upgrade: ({ hash }: {
82
+ update: ({ hash }: {
106
83
  hash: Buffer;
107
84
  }, options?: {
108
85
  /**
@@ -123,9 +100,8 @@ export declare class Client extends ContractClient {
123
100
  readonly options: ContractClientOptions;
124
101
  constructor(options: ContractClientOptions);
125
102
  readonly fromJSON: {
126
- init: (json: string) => AssembledTransaction<Result<void, import("@stellar/stellar-sdk/contract").ErrorMessage>>;
127
103
  add: (json: string) => AssembledTransaction<Result<void, import("@stellar/stellar-sdk/contract").ErrorMessage>>;
128
104
  remove: (json: string) => AssembledTransaction<Result<void, import("@stellar/stellar-sdk/contract").ErrorMessage>>;
129
- upgrade: (json: string) => AssembledTransaction<Result<void, import("@stellar/stellar-sdk/contract").ErrorMessage>>;
105
+ update: (json: string) => AssembledTransaction<Result<void, import("@stellar/stellar-sdk/contract").ErrorMessage>>;
130
106
  };
131
107
  }
package/src/base.ts CHANGED
@@ -1,14 +1,40 @@
1
+ import { Networks, SorobanRpc, xdr } from "@stellar/stellar-sdk"
2
+ import { Client as FactoryClient } from 'passkey-factory-sdk'
3
+ import base64url from "base64url"
4
+
1
5
  export class PasskeyBase {
6
+ public rpc: SorobanRpc.Server
7
+ public factory: FactoryClient
8
+ public rpcUrl: string
9
+ public networkPassphrase: Networks
2
10
  public launchtubeUrl: string | undefined
3
11
  public launchtubeJwt: string | undefined
12
+ public mercuryUrl: string | undefined
13
+ public mercuryJwt: string | undefined
14
+ public mercuryEmail: string | undefined
15
+ public mercuryPassword: string | undefined
4
16
 
5
17
  constructor(options: {
18
+ rpcUrl: string,
19
+ networkPassphrase: string,
20
+ factoryContractId: string,
6
21
  launchtubeUrl?: string,
7
22
  launchtubeJwt?: string,
23
+ mercuryUrl?: string,
24
+ mercuryJwt?: string,
25
+ mercuryEmail?: string,
26
+ mercuryPassword?: string
8
27
  }) {
9
28
  const {
29
+ rpcUrl,
30
+ networkPassphrase,
31
+ factoryContractId,
10
32
  launchtubeUrl,
11
33
  launchtubeJwt,
34
+ mercuryUrl,
35
+ mercuryJwt,
36
+ mercuryEmail,
37
+ mercuryPassword
12
38
  } = options
13
39
 
14
40
  if (launchtubeUrl)
@@ -16,8 +42,139 @@ export class PasskeyBase {
16
42
 
17
43
  if (launchtubeJwt)
18
44
  this.launchtubeJwt = launchtubeJwt
45
+
46
+ if (mercuryUrl)
47
+ this.mercuryUrl = mercuryUrl
48
+
49
+ if (mercuryJwt)
50
+ this.mercuryJwt = mercuryJwt
51
+
52
+ if (mercuryEmail)
53
+ this.mercuryEmail = mercuryEmail
54
+
55
+ if (mercuryPassword)
56
+ this.mercuryPassword = mercuryPassword
57
+
58
+ this.rpcUrl = rpcUrl
59
+ this.rpc = new SorobanRpc.Server(rpcUrl)
60
+ this.networkPassphrase = networkPassphrase as Networks
61
+ this.factory = new FactoryClient({
62
+ contractId: factoryContractId,
63
+ networkPassphrase,
64
+ rpcUrl
65
+ })
19
66
  }
20
67
 
68
+ public async getMercuryJwt() {
69
+ if (!this.mercuryEmail || !this.mercuryPassword)
70
+ throw new Error('Mercury service not configured')
71
+
72
+ const { data: { authenticate: { jwtToken } } } = await fetch(`${this.mercuryUrl}/graphql`, {
73
+ method: 'POST',
74
+ headers: {
75
+ 'Content-Type': 'application/json',
76
+ },
77
+ body: JSON.stringify({
78
+ query: `mutation {
79
+ authenticate(input: {
80
+ email: "${this.mercuryEmail}"
81
+ password: "${this.mercuryPassword}"
82
+ }) {
83
+ jwtToken
84
+ }
85
+ }`
86
+ })
87
+ })
88
+ .then(async (res) => {
89
+ if (res.ok)
90
+ return res.json()
91
+
92
+ throw await res.json()
93
+ })
94
+
95
+ return jwtToken
96
+ }
97
+
98
+ public async getSigners(contractId: string = this.factory.options.contractId) {
99
+ if (!this.mercuryUrl || !this.mercuryJwt)
100
+ throw new Error('Mercury service not configured')
101
+
102
+ const signers = await fetch(`${this.mercuryUrl}/zephyr/execute`, {
103
+ method: 'POST',
104
+ headers: {
105
+ 'Content-Type': 'application/json',
106
+ Authorization: `Bearer ${this.mercuryJwt}`
107
+ },
108
+ body: JSON.stringify({
109
+ mode: {
110
+ Function: {
111
+ fname: "get_signers_by_address",
112
+ arguments: JSON.stringify({
113
+ address: contractId
114
+ })
115
+ }
116
+ }
117
+ })
118
+ })
119
+ .then(async (res) => {
120
+ if (res.ok)
121
+ return res.json()
122
+
123
+ throw await res.json()
124
+ })
125
+
126
+ for (const signer of signers) {
127
+ if (!signer.admin) {
128
+ try {
129
+ await this.rpc.getContractData(contractId, xdr.ScVal.scvBytes(signer.id), SorobanRpc.Durability.Temporary)
130
+ } catch {
131
+ signer.expired = true
132
+ }
133
+ }
134
+
135
+ signer.id = base64url(signer.id)
136
+ signer.pk = base64url(signer.pk)
137
+ }
138
+
139
+ return signers as { id: string, pk: string, admin: boolean, expired?: boolean }[]
140
+ }
141
+
142
+ public async getContractId(keyId: string) {
143
+ if (!this.mercuryUrl || !this.mercuryJwt)
144
+ return
145
+
146
+ const res = await fetch(`${this.mercuryUrl}/zephyr/execute`, {
147
+ method: 'POST',
148
+ headers: {
149
+ 'Content-Type': 'application/json',
150
+ Authorization: `Bearer ${this.mercuryJwt}`
151
+ },
152
+ body: JSON.stringify({
153
+ mode: {
154
+ Function: {
155
+ fname: "get_address_by_signer",
156
+ arguments: JSON.stringify({
157
+ id: [...base64url.toBuffer(keyId)]
158
+ })
159
+ }
160
+ }
161
+ })
162
+ })
163
+ .then(async (res) => {
164
+ if (res.ok)
165
+ return res.json()
166
+
167
+ throw await res.json()
168
+ })
169
+
170
+ return res[0]?.address as string | undefined
171
+ }
172
+
173
+ /* TODO
174
+ - Add a method for getting a paginated or filtered list of all a wallet's events
175
+ @Later
176
+ */
177
+
21
178
  public async send(xdr: string, fee: number = 10_000) {
22
179
  if (!this.launchtubeUrl || !this.launchtubeJwt)
23
180
  throw new Error('Launchtube service not configured')
package/src/kit.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Client as PasskeyClient } from 'passkey-kit-sdk'
2
- import { Client as FactoryClient } from 'passkey-factory-sdk'
3
- import { Address, Networks, StrKey, hash, xdr, Transaction, SorobanRpc, Operation, TransactionBuilder } from '@stellar/stellar-sdk'
2
+
3
+ import { Address, StrKey, hash, xdr, Transaction, SorobanRpc, Operation, TransactionBuilder } from '@stellar/stellar-sdk'
4
4
  import base64url from 'base64url'
5
5
  import { startRegistration, startAuthentication } from "@simplewebauthn/browser"
6
6
  import type { AuthenticatorAttestationResponseJSON } from "@simplewebauthn/types"
@@ -8,53 +8,31 @@ import { decode } from 'cbor-x/decode'
8
8
  import { Buffer } from 'buffer'
9
9
  import { PasskeyBase } from './base'
10
10
 
11
- type GetContractIdFunction = (keyId: string) => Promise<string>;
12
-
13
11
  export class PasskeyKit extends PasskeyBase {
14
12
  public keyId: string | undefined
15
- public keyExpired: boolean | undefined
16
13
  public wallet: PasskeyClient | undefined
17
- public factory: FactoryClient
18
- public networkPassphrase: Networks
19
- public rpcUrl: string
20
- public rpc: SorobanRpc.Server
21
14
 
22
15
  constructor(options: {
23
16
  rpcUrl: string,
24
- launchtubeUrl?: string,
25
- launchtubeJwt?: string,
26
17
  networkPassphrase: string,
27
18
  factoryContractId: string,
19
+ launchtubeUrl?: string,
20
+ launchtubeJwt?: string,
21
+ mercuryUrl?: string,
22
+ mercuryJwt?: string,
23
+ mercuryEmail?: string,
24
+ mercuryPassword?: string
28
25
  }) {
29
- const {
30
- rpcUrl,
31
- launchtubeUrl,
32
- launchtubeJwt,
33
- networkPassphrase,
34
- factoryContractId,
35
- } = options
36
-
37
- super({
38
- launchtubeUrl,
39
- launchtubeJwt,
40
- })
41
-
42
- this.rpcUrl = rpcUrl
43
- this.rpc = new SorobanRpc.Server(rpcUrl)
44
- this.networkPassphrase = networkPassphrase as Networks
45
- this.factory = new FactoryClient({
46
- contractId: factoryContractId,
47
- networkPassphrase,
48
- rpcUrl
49
- })
26
+ super(options)
50
27
  }
51
28
 
52
29
  public async createWallet(app: string, user: string) {
53
30
  const { keyId, publicKey } = await this.createKey(app, user)
54
31
 
55
32
  const { result, built } = await this.factory.deploy({
33
+ salt: hash(keyId),
56
34
  id: keyId,
57
- pk: publicKey!
35
+ pk: publicKey
58
36
  })
59
37
 
60
38
  const contractId = result.unwrap() as string
@@ -75,13 +53,13 @@ export class PasskeyKit extends PasskeyBase {
75
53
  public async createKey(app: string, user: string) {
76
54
  const now = new Date()
77
55
  const displayName = `${user} — ${now.toLocaleString()}`
78
- const { id, response} = await startRegistration({
56
+ const { id, response } = await startRegistration({
79
57
  challenge: base64url("stellaristhebetterblockchain"),
80
58
  rp: {
81
59
  // id: undefined,
82
60
  name: app,
83
61
  },
84
- user: { // TODO there's a real danger here of overwriting a user's key if they use the same `user` name
62
+ user: {
85
63
  id: base64url(`${user}:${now.getTime()}:${Math.random()}`),
86
64
  name: displayName,
87
65
  displayName
@@ -93,24 +71,23 @@ export class PasskeyKit extends PasskeyBase {
93
71
  },
94
72
  pubKeyCredParams: [{ alg: -7, type: "public-key" }],
95
73
  attestation: "none",
74
+ timeout: 120_000
96
75
  });
97
76
 
98
77
  if (!this.keyId)
99
78
  this.keyId = id;
100
79
 
101
- const publicKey = this.getPublicKey(response);
102
-
103
80
  return {
104
81
  keyId: base64url.toBuffer(id),
105
- publicKey
82
+ publicKey: this.getPublicKey(response)
106
83
  }
107
84
  }
108
85
 
109
- public async connectWallet(opts: {
86
+ public async connectWallet(opts?: {
110
87
  keyId?: string | Uint8Array,
111
- getContractId?: GetContractIdFunction
88
+ getContractId?: (keyId: string) => Promise<string | undefined>
112
89
  }) {
113
- let { keyId, getContractId } = opts
90
+ let { keyId, getContractId = this.getContractId } = opts || {}
114
91
  let keyIdBuffer: Buffer
115
92
 
116
93
  if (!keyId) {
@@ -118,6 +95,7 @@ export class PasskeyKit extends PasskeyBase {
118
95
  challenge: base64url("stellaristhebetterblockchain"),
119
96
  // rpId: undefined,
120
97
  userVerification: "discouraged",
98
+ timeout: 120_000
121
99
  });
122
100
 
123
101
  console.log(response);
@@ -138,7 +116,7 @@ export class PasskeyKit extends PasskeyBase {
138
116
  // Check for the contractId on-chain as a derivation from the keyId. This is the easiest and "cheapest" check however it will only work for the initially deployed passkey if it was used as derivation
139
117
  let contractId: string | undefined = StrKey.encodeContract(hash(xdr.HashIdPreimage.envelopeTypeContractId(
140
118
  new xdr.HashIdPreimageContractId({
141
- networkId: hash(Buffer.from(this.networkPassphrase, 'utf-8')),
119
+ networkId: hash(Buffer.from(this.networkPassphrase)),
142
120
  contractIdPreimage: xdr.ContractIdPreimage.contractIdPreimageFromAddress(
143
121
  new xdr.ContractIdPreimageFromAddress({
144
122
  address: Address.fromString(this.factory.options.contractId).toScAddress(),
@@ -150,23 +128,12 @@ export class PasskeyKit extends PasskeyBase {
150
128
 
151
129
  // attempt passkey id derivation
152
130
  try {
131
+ // TODO what is the error if the entry exists but is archived?
153
132
  await this.rpc.getContractData(contractId, xdr.ScVal.scvLedgerKeyContractInstance())
154
133
  }
155
- // if that fails look up from the factory mapper
134
+ // if that fails look up from the `getContractId` function
156
135
  catch {
157
- contractId = undefined
158
-
159
- if (getContractId) {
160
- contractId = await getContractId(keyId)
161
-
162
- // Handle case where temporary session signer is missing on-chain (so we can queue up a re-add)
163
- try {
164
- await this.rpc.getContractData(contractId, xdr.ScVal.scvBytes(keyIdBuffer), SorobanRpc.Durability.Temporary)
165
- // throw true
166
- } catch {
167
- this.keyExpired = true
168
- }
169
- }
136
+ contractId = await getContractId(keyId)
170
137
  }
171
138
 
172
139
  if (!contractId)
@@ -213,6 +180,7 @@ export class PasskeyKit extends PasskeyBase {
213
180
  challenge: base64url(payload),
214
181
  // rpId: undefined,
215
182
  userVerification: "discouraged",
183
+ timeout: 120_000
216
184
  }
217
185
  : {
218
186
  challenge: base64url(payload),
@@ -226,6 +194,7 @@ export class PasskeyKit extends PasskeyBase {
226
194
  },
227
195
  ],
228
196
  userVerification: "discouraged",
197
+ timeout: 120_000
229
198
  }
230
199
  );
231
200
 
@@ -255,6 +224,7 @@ export class PasskeyKit extends PasskeyBase {
255
224
 
256
225
  return entry
257
226
  }
227
+
258
228
  public async signAuthEntries(
259
229
  entries: xdr.SorobanAuthorizationEntry[],
260
230
  options?: {
@@ -275,6 +245,7 @@ export class PasskeyKit extends PasskeyBase {
275
245
 
276
246
  return entries
277
247
  }
248
+
278
249
  public async sign(
279
250
  txn: Transaction | string,
280
251
  options?: {
@@ -355,6 +326,7 @@ export class PasskeyKit extends PasskeyBase {
355
326
 
356
327
  return publicKey
357
328
  }
329
+
358
330
  private compactSignature(signature: Buffer) {
359
331
  // Decode the DER signature
360
332
  let offset = 2;
package/types/base.d.ts CHANGED
@@ -1,9 +1,34 @@
1
+ import { Networks, SorobanRpc } from "@stellar/stellar-sdk";
2
+ import { Client as FactoryClient } from 'passkey-factory-sdk';
1
3
  export declare class PasskeyBase {
4
+ rpc: SorobanRpc.Server;
5
+ factory: FactoryClient;
6
+ rpcUrl: string;
7
+ networkPassphrase: Networks;
2
8
  launchtubeUrl: string | undefined;
3
9
  launchtubeJwt: string | undefined;
10
+ mercuryUrl: string | undefined;
11
+ mercuryJwt: string | undefined;
12
+ mercuryEmail: string | undefined;
13
+ mercuryPassword: string | undefined;
4
14
  constructor(options: {
15
+ rpcUrl: string;
16
+ networkPassphrase: string;
17
+ factoryContractId: string;
5
18
  launchtubeUrl?: string;
6
19
  launchtubeJwt?: string;
20
+ mercuryUrl?: string;
21
+ mercuryJwt?: string;
22
+ mercuryEmail?: string;
23
+ mercuryPassword?: string;
7
24
  });
25
+ getMercuryJwt(): Promise<any>;
26
+ getSigners(contractId?: string): Promise<{
27
+ id: string;
28
+ pk: string;
29
+ admin: boolean;
30
+ expired?: boolean;
31
+ }[]>;
32
+ getContractId(keyId: string): Promise<string | undefined>;
8
33
  send(xdr: string, fee?: number): Promise<any>;
9
34
  }
package/types/kit.d.ts CHANGED
@@ -1,23 +1,20 @@
1
1
  import { Client as PasskeyClient } from 'passkey-kit-sdk';
2
- import { Client as FactoryClient } from 'passkey-factory-sdk';
3
- import { Networks, xdr, Transaction, SorobanRpc } from '@stellar/stellar-sdk';
2
+ import { xdr, Transaction } from '@stellar/stellar-sdk';
4
3
  import { Buffer } from 'buffer';
5
4
  import { PasskeyBase } from './base';
6
- type GetContractIdFunction = (keyId: string) => Promise<string>;
7
5
  export declare class PasskeyKit extends PasskeyBase {
8
6
  keyId: string | undefined;
9
- keyExpired: boolean | undefined;
10
7
  wallet: PasskeyClient | undefined;
11
- factory: FactoryClient;
12
- networkPassphrase: Networks;
13
- rpcUrl: string;
14
- rpc: SorobanRpc.Server;
15
8
  constructor(options: {
16
9
  rpcUrl: string;
17
- launchtubeUrl?: string;
18
- launchtubeJwt?: string;
19
10
  networkPassphrase: string;
20
11
  factoryContractId: string;
12
+ launchtubeUrl?: string;
13
+ launchtubeJwt?: string;
14
+ mercuryUrl?: string;
15
+ mercuryJwt?: string;
16
+ mercuryEmail?: string;
17
+ mercuryPassword?: string;
21
18
  });
22
19
  createWallet(app: string, user: string): Promise<{
23
20
  keyId: Buffer;
@@ -28,9 +25,9 @@ export declare class PasskeyKit extends PasskeyBase {
28
25
  keyId: Buffer;
29
26
  publicKey: Buffer;
30
27
  }>;
31
- connectWallet(opts: {
28
+ connectWallet(opts?: {
32
29
  keyId?: string | Uint8Array;
33
- getContractId?: GetContractIdFunction;
30
+ getContractId?: (keyId: string) => Promise<string | undefined>;
34
31
  }): Promise<{
35
32
  keyId: Buffer;
36
33
  contractId: string;
@@ -50,4 +47,3 @@ export declare class PasskeyKit extends PasskeyBase {
50
47
  private getPublicKey;
51
48
  private compactSignature;
52
49
  }
53
- export {};
package/TODO.md DELETED
@@ -1 +0,0 @@
1
- - [ ] Add timestamps to zephyr event data