passkey-kit 0.1.1 → 0.2.1
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/cheatsheet +5 -0
- package/package.json +3 -4
- package/packages/passkey-factory-sdk/package.json +1 -1
- package/packages/passkey-factory-sdk/src/index.ts +3 -5
- package/packages/passkey-factory-sdk/types/index.d.ts +2 -3
- package/packages/passkey-kit-sdk/package.json +1 -1
- package/packages/passkey-kit-sdk/src/index.ts +25 -5
- package/packages/passkey-kit-sdk/types/index.d.ts +22 -3
- package/src/base.ts +42 -0
- package/src/index.ts +3 -506
- package/src/kit.ts +479 -0
- package/types/base.d.ts +9 -0
- package/types/index.d.ts +3 -49
- package/types/{src/index.d.ts → kit.d.ts} +5 -12
package/cheatsheet
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "passkey-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|
|
@@ -9,14 +9,13 @@
|
|
|
9
9
|
"types": "types/index.d.ts",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@simplewebauthn/browser": "^10.0.0",
|
|
12
|
-
"@simplewebauthn/types": "^10.0.0",
|
|
13
12
|
"@stellar/stellar-sdk": "12.0.1",
|
|
14
13
|
"base64url": "^3.0.1",
|
|
15
14
|
"bigint-conversion": "^2.4.3",
|
|
16
15
|
"buffer": "^6.0.3",
|
|
17
16
|
"cbor-x": "^1.5.9",
|
|
18
|
-
"passkey-factory-sdk": "0.0.
|
|
19
|
-
"passkey-kit-sdk": "0.0.
|
|
17
|
+
"passkey-factory-sdk": "0.0.8",
|
|
18
|
+
"passkey-kit-sdk": "0.0.8"
|
|
20
19
|
},
|
|
21
20
|
"devDependencies": {
|
|
22
21
|
"typescript": "^5.4.5"
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { Buffer } from "buffer";
|
|
2
2
|
import {
|
|
3
|
-
Client as ContractClient,
|
|
4
|
-
Spec as ContractSpec,
|
|
5
|
-
} from '@stellar/stellar-sdk/contract';
|
|
6
|
-
import type {
|
|
7
3
|
AssembledTransaction,
|
|
4
|
+
Client as ContractClient,
|
|
8
5
|
ClientOptions as ContractClientOptions,
|
|
9
6
|
Result,
|
|
7
|
+
Spec as ContractSpec,
|
|
10
8
|
} from '@stellar/stellar-sdk/contract';
|
|
11
9
|
|
|
12
10
|
if (typeof window !== 'undefined') {
|
|
@@ -17,7 +15,7 @@ if (typeof window !== 'undefined') {
|
|
|
17
15
|
export const networks = {
|
|
18
16
|
testnet: {
|
|
19
17
|
networkPassphrase: "Test SDF Network ; September 2015",
|
|
20
|
-
contractId: "
|
|
18
|
+
contractId: "CBXQAM53YVVPKAPS2NCSOCXTOK6HOXA3JLURVLD6VFQUHJPKFVBJIBHQ",
|
|
21
19
|
}
|
|
22
20
|
} as const
|
|
23
21
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Buffer } from "buffer";
|
|
2
|
-
import { Client as ContractClient } from '@stellar/stellar-sdk/contract';
|
|
3
|
-
import type { AssembledTransaction, ClientOptions as ContractClientOptions, Result } from '@stellar/stellar-sdk/contract';
|
|
2
|
+
import { AssembledTransaction, Client as ContractClient, ClientOptions as ContractClientOptions, Result } from '@stellar/stellar-sdk/contract';
|
|
4
3
|
export declare const networks: {
|
|
5
4
|
readonly testnet: {
|
|
6
5
|
readonly networkPassphrase: "Test SDF Network ; September 2015";
|
|
7
|
-
readonly contractId: "
|
|
6
|
+
readonly contractId: "CBXQAM53YVVPKAPS2NCSOCXTOK6HOXA3JLURVLD6VFQUHJPKFVBJIBHQ";
|
|
8
7
|
};
|
|
9
8
|
};
|
|
10
9
|
export declare const Errors: {
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { Buffer } from "buffer";
|
|
2
2
|
import {
|
|
3
|
-
Client as ContractClient,
|
|
4
|
-
Spec as ContractSpec,
|
|
5
|
-
} from '@stellar/stellar-sdk/contract';
|
|
6
|
-
import type {
|
|
7
3
|
AssembledTransaction,
|
|
4
|
+
Client as ContractClient,
|
|
8
5
|
ClientOptions as ContractClientOptions,
|
|
9
6
|
Result,
|
|
7
|
+
Spec as ContractSpec,
|
|
10
8
|
} from '@stellar/stellar-sdk/contract';
|
|
11
9
|
|
|
12
10
|
if (typeof window !== 'undefined') {
|
|
@@ -17,7 +15,7 @@ if (typeof window !== 'undefined') {
|
|
|
17
15
|
export const networks = {
|
|
18
16
|
testnet: {
|
|
19
17
|
networkPassphrase: "Test SDF Network ; September 2015",
|
|
20
|
-
contractId: "
|
|
18
|
+
contractId: "CBXQAM53YVVPKAPS2NCSOCXTOK6HOXA3JLURVLD6VFQUHJPKFVBJIBHQ",
|
|
21
19
|
}
|
|
22
20
|
} as const
|
|
23
21
|
|
|
@@ -82,6 +80,26 @@ export interface Client {
|
|
|
82
80
|
simulate?: boolean;
|
|
83
81
|
}) => Promise<AssembledTransaction<Result<void>>>
|
|
84
82
|
|
|
83
|
+
/**
|
|
84
|
+
* 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.
|
|
85
|
+
*/
|
|
86
|
+
upgrade: ({ hash }: { hash: Buffer }, options?: {
|
|
87
|
+
/**
|
|
88
|
+
* The fee to pay for the transaction. Default: BASE_FEE
|
|
89
|
+
*/
|
|
90
|
+
fee?: number;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT
|
|
94
|
+
*/
|
|
95
|
+
timeoutInSeconds?: number;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true
|
|
99
|
+
*/
|
|
100
|
+
simulate?: boolean;
|
|
101
|
+
}) => Promise<AssembledTransaction<Result<void>>>
|
|
102
|
+
|
|
85
103
|
/**
|
|
86
104
|
* Construct and simulate a resudo 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.
|
|
87
105
|
*/
|
|
@@ -148,6 +166,7 @@ export class Client extends ContractClient {
|
|
|
148
166
|
new ContractSpec(["AAAABAAAAAAAAAAAAAAABUVycm9yAAAAAAAACgAAAAAAAAAJTm90SW5pdGVkAAAAAAAAAQAAAAAAAAAITm90Rm91bmQAAAACAAAAAAAAAAxOb3RQZXJtaXR0ZWQAAAADAAAAAAAAAA1BbHJlYWR5SW5pdGVkAAAAAAAABAAAAAAAAAAOSnNvblBhcnNlRXJyb3IAAAAAAAUAAAAAAAAADkludmFsaWRDb250ZXh0AAAAAAAGAAAAAAAAACBDbGllbnREYXRhSnNvbkNoYWxsZW5nZUluY29ycmVjdAAAAAcAAAAAAAAAF1NlY3AyNTZyMVB1YmxpY0tleVBhcnNlAAAAAAgAAAAAAAAAF1NlY3AyNTZyMVNpZ25hdHVyZVBhcnNlAAAAAAkAAAAAAAAAFVNlY3AyNTZyMVZlcmlmeUZhaWxlZAAAAAAAAAo=",
|
|
149
167
|
"AAAAAAAAAAAAAAAKZXh0ZW5kX3R0bAAAAAAAAAAAAAA=",
|
|
150
168
|
"AAAAAAAAAAAAAAAEaW5pdAAAAAMAAAAAAAAAAmlkAAAAAAAOAAAAAAAAAAJwawAAAAAD7gAAAEEAAAAAAAAAB2ZhY3RvcnkAAAAAEwAAAAEAAAPpAAAD7QAAAAAAAAAD",
|
|
169
|
+
"AAAAAAAAAAAAAAAHdXBncmFkZQAAAAABAAAAAAAAAARoYXNoAAAD7gAAACAAAAABAAAD6QAAA+0AAAAAAAAAAw==",
|
|
151
170
|
"AAAAAAAAAAAAAAAGcmVzdWRvAAAAAAABAAAAAAAAAAJpZAAAAAAADgAAAAEAAAPpAAAD7QAAAAAAAAAD",
|
|
152
171
|
"AAAAAAAAAAAAAAAGcm1fc2lnAAAAAAABAAAAAAAAAAJpZAAAAAAADgAAAAEAAAPpAAAD7QAAAAAAAAAD",
|
|
153
172
|
"AAAAAAAAAAAAAAAHYWRkX3NpZwAAAAACAAAAAAAAAAJpZAAAAAAADgAAAAAAAAACcGsAAAAAA+4AAABBAAAAAQAAA+kAAAPtAAAAAAAAAAM=",
|
|
@@ -159,6 +178,7 @@ export class Client extends ContractClient {
|
|
|
159
178
|
public readonly fromJSON = {
|
|
160
179
|
extend_ttl: this.txFromJSON<null>,
|
|
161
180
|
init: this.txFromJSON<Result<void>>,
|
|
181
|
+
upgrade: this.txFromJSON<Result<void>>,
|
|
162
182
|
resudo: this.txFromJSON<Result<void>>,
|
|
163
183
|
rm_sig: this.txFromJSON<Result<void>>,
|
|
164
184
|
add_sig: this.txFromJSON<Result<void>>,
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Buffer } from "buffer";
|
|
2
|
-
import { Client as ContractClient } from '@stellar/stellar-sdk/contract';
|
|
3
|
-
import type { AssembledTransaction, ClientOptions as ContractClientOptions, Result } from '@stellar/stellar-sdk/contract';
|
|
2
|
+
import { AssembledTransaction, Client as ContractClient, ClientOptions as ContractClientOptions, Result } from '@stellar/stellar-sdk/contract';
|
|
4
3
|
export declare const networks: {
|
|
5
4
|
readonly testnet: {
|
|
6
5
|
readonly networkPassphrase: "Test SDF Network ; September 2015";
|
|
7
|
-
readonly contractId: "
|
|
6
|
+
readonly contractId: "CBXQAM53YVVPKAPS2NCSOCXTOK6HOXA3JLURVLD6VFQUHJPKFVBJIBHQ";
|
|
8
7
|
};
|
|
9
8
|
};
|
|
10
9
|
export declare const Errors: {
|
|
@@ -84,6 +83,25 @@ export interface Client {
|
|
|
84
83
|
*/
|
|
85
84
|
simulate?: boolean;
|
|
86
85
|
}) => Promise<AssembledTransaction<Result<void>>>;
|
|
86
|
+
/**
|
|
87
|
+
* 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.
|
|
88
|
+
*/
|
|
89
|
+
upgrade: ({ hash }: {
|
|
90
|
+
hash: Buffer;
|
|
91
|
+
}, options?: {
|
|
92
|
+
/**
|
|
93
|
+
* The fee to pay for the transaction. Default: BASE_FEE
|
|
94
|
+
*/
|
|
95
|
+
fee?: number;
|
|
96
|
+
/**
|
|
97
|
+
* The maximum amount of time to wait for the transaction to complete. Default: DEFAULT_TIMEOUT
|
|
98
|
+
*/
|
|
99
|
+
timeoutInSeconds?: number;
|
|
100
|
+
/**
|
|
101
|
+
* Whether to automatically simulate the transaction when constructing the AssembledTransaction. Default: true
|
|
102
|
+
*/
|
|
103
|
+
simulate?: boolean;
|
|
104
|
+
}) => Promise<AssembledTransaction<Result<void>>>;
|
|
87
105
|
/**
|
|
88
106
|
* Construct and simulate a resudo 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.
|
|
89
107
|
*/
|
|
@@ -149,6 +167,7 @@ export declare class Client extends ContractClient {
|
|
|
149
167
|
readonly fromJSON: {
|
|
150
168
|
extend_ttl: (json: string) => AssembledTransaction<null>;
|
|
151
169
|
init: (json: string) => AssembledTransaction<Result<void, import("@stellar/stellar-sdk/contract").ErrorMessage>>;
|
|
170
|
+
upgrade: (json: string) => AssembledTransaction<Result<void, import("@stellar/stellar-sdk/contract").ErrorMessage>>;
|
|
152
171
|
resudo: (json: string) => AssembledTransaction<Result<void, import("@stellar/stellar-sdk/contract").ErrorMessage>>;
|
|
153
172
|
rm_sig: (json: string) => AssembledTransaction<Result<void, import("@stellar/stellar-sdk/contract").ErrorMessage>>;
|
|
154
173
|
add_sig: (json: string) => AssembledTransaction<Result<void, import("@stellar/stellar-sdk/contract").ErrorMessage>>;
|
package/src/base.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export class PasskeyBase {
|
|
2
|
+
public launchtubeUrl: string | undefined
|
|
3
|
+
public launchtubeJwt: string | undefined
|
|
4
|
+
|
|
5
|
+
constructor(options: {
|
|
6
|
+
launchtubeUrl?: string,
|
|
7
|
+
launchtubeJwt?: string,
|
|
8
|
+
}) {
|
|
9
|
+
const {
|
|
10
|
+
launchtubeUrl,
|
|
11
|
+
launchtubeJwt,
|
|
12
|
+
} = options
|
|
13
|
+
|
|
14
|
+
if (launchtubeUrl)
|
|
15
|
+
this.launchtubeUrl = launchtubeUrl
|
|
16
|
+
|
|
17
|
+
if (launchtubeJwt)
|
|
18
|
+
this.launchtubeJwt = launchtubeJwt
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async send(xdr: string, fee: number = 10_000) {
|
|
22
|
+
if (!this.launchtubeUrl || !this.launchtubeJwt)
|
|
23
|
+
throw new Error('Launchtube service not configured')
|
|
24
|
+
|
|
25
|
+
const data = new FormData();
|
|
26
|
+
|
|
27
|
+
data.set('xdr', xdr);
|
|
28
|
+
data.set('fee', fee.toString());
|
|
29
|
+
|
|
30
|
+
return fetch(this.launchtubeUrl, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: {
|
|
33
|
+
authorization: `Bearer ${this.launchtubeJwt}`,
|
|
34
|
+
},
|
|
35
|
+
body: data
|
|
36
|
+
}).then(async (res) => {
|
|
37
|
+
if (res.ok)
|
|
38
|
+
return res.json()
|
|
39
|
+
else throw await res.json()
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,507 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { Address, Networks, StrKey, hash, xdr, Transaction, Horizon, FeeBumpTransaction, SorobanRpc, Operation, scValToNative, TransactionBuilder } from '@stellar/stellar-sdk'
|
|
4
|
-
import { bufToBigint, bigintToBuf } from 'bigint-conversion'
|
|
5
|
-
import base64url from 'base64url'
|
|
6
|
-
import { startRegistration, startAuthentication } from "@simplewebauthn/browser"
|
|
7
|
-
import { decode } from 'cbor-x/decode'
|
|
8
|
-
import type { RegistrationResponseJSON } from '@simplewebauthn/types';
|
|
9
|
-
import { Buffer } from 'buffer'
|
|
1
|
+
import { PasskeyBase } from "./base"
|
|
2
|
+
import { PasskeyKit } from "./kit"
|
|
10
3
|
|
|
11
|
-
|
|
12
|
-
- Clean up these params and the interface as a whole
|
|
13
|
-
Might put wallet activities and maybe factory as well into the root of the class vs buried inside this.wallet and this.factory
|
|
14
|
-
@Later
|
|
15
|
-
|
|
16
|
-
- Right now publicKey can mean a Stellar public key or a passkey public key, there should be a noted difference
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
export class PasskeyAccount {
|
|
20
|
-
public keyId: string | undefined
|
|
21
|
-
public sudoKeyId: string | undefined
|
|
22
|
-
public wallet: PasskeyClient | undefined
|
|
23
|
-
public factory: FactoryClient
|
|
24
|
-
public sequencePublicKey: string
|
|
25
|
-
public networkPassphrase: Networks
|
|
26
|
-
public horizonUrl: string
|
|
27
|
-
public horizon: Horizon.Server
|
|
28
|
-
public rpcUrl: string
|
|
29
|
-
public rpc: SorobanRpc.Server
|
|
30
|
-
public feeBumpUrl: string | undefined
|
|
31
|
-
public feeBumpJwt: string | undefined
|
|
32
|
-
public factoryContractId: string = networks.testnet.contractId
|
|
33
|
-
|
|
34
|
-
/* TODO
|
|
35
|
-
- Consider adding the ability to pass in a keyId and maybe even a contractId in order to preconnect to a wallet
|
|
36
|
-
If just a keyId call `connectWallet` in order to get the contractId
|
|
37
|
-
If both keyId and contractId are passed in then we can skip the connectWallet call (though we won't get the sudoKeyId in that case)
|
|
38
|
-
We don't stictly _need_ this as a dev can just call `connectWallet` after class instantiation but it might be a nice convenience
|
|
39
|
-
@Later
|
|
40
|
-
*/
|
|
41
|
-
constructor(options: {
|
|
42
|
-
sequencePublicKey: string,
|
|
43
|
-
networkPassphrase: Networks,
|
|
44
|
-
horizonUrl: string,
|
|
45
|
-
rpcUrl: string,
|
|
46
|
-
feeBumpUrl?: string,
|
|
47
|
-
feeBumpJwt?: string,
|
|
48
|
-
/* TODO
|
|
49
|
-
- Maybe remove this? The factory should likely be baked in a bit more tightly
|
|
50
|
-
On the other hand once we have a standard interface the factory interface only uses the `deploy` method right now inside the interface
|
|
51
|
-
@Later
|
|
52
|
-
*/
|
|
53
|
-
factoryContractId?: string,
|
|
54
|
-
}) {
|
|
55
|
-
const {
|
|
56
|
-
sequencePublicKey,
|
|
57
|
-
networkPassphrase,
|
|
58
|
-
horizonUrl,
|
|
59
|
-
rpcUrl,
|
|
60
|
-
feeBumpUrl,
|
|
61
|
-
feeBumpJwt,
|
|
62
|
-
factoryContractId
|
|
63
|
-
} = options
|
|
64
|
-
|
|
65
|
-
this.sequencePublicKey = sequencePublicKey
|
|
66
|
-
this.networkPassphrase = networkPassphrase
|
|
67
|
-
this.horizonUrl = horizonUrl
|
|
68
|
-
this.horizon = new Horizon.Server(horizonUrl)
|
|
69
|
-
this.rpcUrl = rpcUrl
|
|
70
|
-
this.rpc = new SorobanRpc.Server(rpcUrl)
|
|
71
|
-
|
|
72
|
-
if (feeBumpUrl)
|
|
73
|
-
this.feeBumpUrl = feeBumpUrl
|
|
74
|
-
|
|
75
|
-
if (feeBumpJwt)
|
|
76
|
-
this.feeBumpJwt = feeBumpJwt
|
|
77
|
-
|
|
78
|
-
if (factoryContractId)
|
|
79
|
-
this.factoryContractId = factoryContractId
|
|
80
|
-
|
|
81
|
-
this.factory = new FactoryClient({
|
|
82
|
-
publicKey: sequencePublicKey,
|
|
83
|
-
contractId: this.factoryContractId,
|
|
84
|
-
networkPassphrase,
|
|
85
|
-
rpcUrl
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
public async createWallet(name: string, user: string) {
|
|
90
|
-
const { keyId, publicKey } = await this.createKey(name, user)
|
|
91
|
-
|
|
92
|
-
const { result, built } = await this.factory.deploy({
|
|
93
|
-
id: keyId,
|
|
94
|
-
pk: publicKey!
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
const contractId = result.unwrap() as string
|
|
98
|
-
|
|
99
|
-
this.wallet = new PasskeyClient({
|
|
100
|
-
publicKey: this.sequencePublicKey,
|
|
101
|
-
contractId,
|
|
102
|
-
networkPassphrase: this.networkPassphrase,
|
|
103
|
-
rpcUrl: this.rpcUrl
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
keyId,
|
|
108
|
-
contractId,
|
|
109
|
-
xdr: built!.toXDR() as string
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
public async createKey(name: string, user: string) {
|
|
114
|
-
const startRegistrationResponse = await startRegistration({
|
|
115
|
-
challenge: base64url("stellaristhebetterblockchain"),
|
|
116
|
-
rp: {
|
|
117
|
-
// id: undefined,
|
|
118
|
-
name,
|
|
119
|
-
},
|
|
120
|
-
user: {
|
|
121
|
-
id: base64url(user),
|
|
122
|
-
name: user,
|
|
123
|
-
displayName: user,
|
|
124
|
-
},
|
|
125
|
-
authenticatorSelection: {
|
|
126
|
-
requireResidentKey: false,
|
|
127
|
-
residentKey: "preferred",
|
|
128
|
-
userVerification: "discouraged",
|
|
129
|
-
},
|
|
130
|
-
pubKeyCredParams: [{ alg: -7, type: "public-key" }],
|
|
131
|
-
attestation: "none",
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
if (!this.keyId) {
|
|
135
|
-
this.keyId = startRegistrationResponse.id
|
|
136
|
-
|
|
137
|
-
// If there was no keyId we're likely about to deploy a new wallet so we should set the sudoKeyId
|
|
138
|
-
if (!this.sudoKeyId)
|
|
139
|
-
this.sudoKeyId = startRegistrationResponse.id
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const { publicKeyObject } = this.getPublicKeyObject(startRegistrationResponse.response.attestationObject);
|
|
143
|
-
|
|
144
|
-
const publicKey = Buffer.from([
|
|
145
|
-
4, // (0x04 prefix) https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm
|
|
146
|
-
...publicKeyObject.get('-2')!,
|
|
147
|
-
...publicKeyObject.get('-3')!
|
|
148
|
-
])
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
keyId: base64url.toBuffer(startRegistrationResponse.id),
|
|
152
|
-
publicKey
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
public async connectWallet(id?: string) {
|
|
157
|
-
/* TODO
|
|
158
|
-
- Support passing in a contractId as well as a keyId
|
|
159
|
-
Maybe not as we wouldn't have a keyId which could have interesting consequences
|
|
160
|
-
Also not sure what the use case would be for this where keyId wouldn't also be possible and better
|
|
161
|
-
@No
|
|
162
|
-
*/
|
|
163
|
-
|
|
164
|
-
// @ts-ignore
|
|
165
|
-
// https://github.com/stellar/js-stellar-base/issues/750
|
|
166
|
-
// if (id && StrKey.isValidContract(id)) {
|
|
167
|
-
|
|
168
|
-
// } else {
|
|
169
|
-
|
|
170
|
-
// }
|
|
171
|
-
|
|
172
|
-
const startAuthenticationResponse = id
|
|
173
|
-
? { id }
|
|
174
|
-
: await startAuthentication({
|
|
175
|
-
challenge: base64url("stellaristhebetterblockchain"),
|
|
176
|
-
// rpId: undefined,
|
|
177
|
-
userVerification: "discouraged",
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
if (!this.keyId)
|
|
181
|
-
this.keyId = startAuthenticationResponse.id
|
|
182
|
-
|
|
183
|
-
const keyIdBuffer = base64url.toBuffer(startAuthenticationResponse.id)
|
|
184
|
-
|
|
185
|
-
// NOTE might not need this for derivation as all signers are stored in the factory and we can use that lookup as both primary and secondary
|
|
186
|
-
let contractId = StrKey.encodeContract(hash(xdr.HashIdPreimage.envelopeTypeContractId(
|
|
187
|
-
new xdr.HashIdPreimageContractId({
|
|
188
|
-
networkId: hash(Buffer.from(this.networkPassphrase, 'utf-8')),
|
|
189
|
-
contractIdPreimage: xdr.ContractIdPreimage.contractIdPreimageFromAddress(
|
|
190
|
-
new xdr.ContractIdPreimageFromAddress({
|
|
191
|
-
address: Address.fromString(this.factoryContractId).toScAddress(),
|
|
192
|
-
salt: hash(keyIdBuffer),
|
|
193
|
-
})
|
|
194
|
-
)
|
|
195
|
-
})
|
|
196
|
-
).toXDR()));
|
|
197
|
-
|
|
198
|
-
// attempt passkey id derivation
|
|
199
|
-
try {
|
|
200
|
-
await this.rpc.getContractData(contractId, xdr.ScVal.scvLedgerKeyContractInstance())
|
|
201
|
-
}
|
|
202
|
-
// if that fails look up from the factory mapper
|
|
203
|
-
catch {
|
|
204
|
-
const { val } = await this.rpc.getContractData(this.factoryContractId, xdr.ScVal.scvBytes(keyIdBuffer))
|
|
205
|
-
contractId = scValToNative(val.contractData().val())
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
this.wallet = new PasskeyClient({
|
|
209
|
-
publicKey: this.sequencePublicKey,
|
|
210
|
-
contractId,
|
|
211
|
-
networkPassphrase: this.networkPassphrase,
|
|
212
|
-
rpcUrl: this.rpcUrl
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
// get and set the sudo signer
|
|
216
|
-
await this.getData()
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
keyId: keyIdBuffer,
|
|
220
|
-
contractId
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
public async sign(
|
|
225
|
-
txn: Transaction | string,
|
|
226
|
-
options?: {
|
|
227
|
-
keyId?: 'any' | 'sudo' | string | Uint8Array
|
|
228
|
-
ledgersToLive?: number
|
|
229
|
-
}
|
|
230
|
-
) {
|
|
231
|
-
// Default mirrors DEFAULT_TIMEOUT (currently 5 minutes) https://github.com/stellar/js-stellar-sdk/blob/master/src/contract/utils.ts#L7
|
|
232
|
-
let { keyId, ledgersToLive = 60 } = options || {}
|
|
233
|
-
|
|
234
|
-
// hack to ensure we don't stack fees when simulating and assembling multiple times
|
|
235
|
-
txn = TransactionBuilder.cloneFrom(new Transaction(
|
|
236
|
-
typeof txn === 'string'
|
|
237
|
-
? txn
|
|
238
|
-
: txn.toXDR(),
|
|
239
|
-
this.networkPassphrase
|
|
240
|
-
), { fee: '0' }).build()
|
|
241
|
-
|
|
242
|
-
// NOTE hard coded to sign only Soroban transactions and only and always the first auth
|
|
243
|
-
// TODO switch to support signing multiple and specific auth entries. Possibly pass in auth entry vs the entire txn
|
|
244
|
-
const op = txn.operations[0] as Operation.InvokeHostFunction
|
|
245
|
-
const auth = op.auth![0]
|
|
246
|
-
const lastLedger = await this.rpc.getLatestLedger().then(({ sequence }) => sequence)
|
|
247
|
-
const authHash = hash(
|
|
248
|
-
xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
|
|
249
|
-
new xdr.HashIdPreimageSorobanAuthorization({
|
|
250
|
-
networkId: hash(Buffer.from(this.networkPassphrase, 'utf-8')),
|
|
251
|
-
nonce: auth.credentials().address().nonce(),
|
|
252
|
-
signatureExpirationLedger: lastLedger + ledgersToLive,
|
|
253
|
-
invocation: auth.rootInvocation()
|
|
254
|
-
})
|
|
255
|
-
).toXDR()
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
const authenticationResponse = await startAuthentication(
|
|
259
|
-
keyId === 'any' || (keyId === 'sudo' && !this.sudoKeyId) || (!keyId && !this.keyId)
|
|
260
|
-
? {
|
|
261
|
-
challenge: base64url(authHash),
|
|
262
|
-
// rpId: undefined,
|
|
263
|
-
userVerification: "discouraged",
|
|
264
|
-
}
|
|
265
|
-
: {
|
|
266
|
-
challenge: base64url(authHash),
|
|
267
|
-
// rpId: undefined,
|
|
268
|
-
allowCredentials: [
|
|
269
|
-
{
|
|
270
|
-
id: keyId === 'sudo'
|
|
271
|
-
? this.sudoKeyId!
|
|
272
|
-
: keyId instanceof Uint8Array
|
|
273
|
-
? base64url(keyId)
|
|
274
|
-
: keyId || this.keyId!,
|
|
275
|
-
type: "public-key",
|
|
276
|
-
},
|
|
277
|
-
],
|
|
278
|
-
userVerification: "discouraged",
|
|
279
|
-
}
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
// set sudo if this is a sudo request
|
|
283
|
-
if (keyId === 'sudo')
|
|
284
|
-
this.sudoKeyId = authenticationResponse.id
|
|
285
|
-
|
|
286
|
-
// reset this.keyId to be the most recently used passkey
|
|
287
|
-
this.keyId = authenticationResponse.id
|
|
288
|
-
|
|
289
|
-
const signatureRaw = base64url.toBuffer(authenticationResponse.response.signature);
|
|
290
|
-
const signature = this.convertEcdsaSignatureAsnToCompact(signatureRaw);
|
|
291
|
-
const creds = auth.credentials().address();
|
|
292
|
-
|
|
293
|
-
creds.signatureExpirationLedger(lastLedger + ledgersToLive)
|
|
294
|
-
creds.signature(xdr.ScVal.scvMap([
|
|
295
|
-
new xdr.ScMapEntry({
|
|
296
|
-
key: xdr.ScVal.scvSymbol('authenticator_data'),
|
|
297
|
-
val: xdr.ScVal.scvBytes(base64url.toBuffer(authenticationResponse.response.authenticatorData)),
|
|
298
|
-
}),
|
|
299
|
-
new xdr.ScMapEntry({
|
|
300
|
-
key: xdr.ScVal.scvSymbol('client_data_json'),
|
|
301
|
-
val: xdr.ScVal.scvBytes(base64url.toBuffer(authenticationResponse.response.clientDataJSON)),
|
|
302
|
-
}),
|
|
303
|
-
new xdr.ScMapEntry({
|
|
304
|
-
key: xdr.ScVal.scvSymbol('id'),
|
|
305
|
-
val: xdr.ScVal.scvBytes(base64url.toBuffer(authenticationResponse.id)),
|
|
306
|
-
}),
|
|
307
|
-
new xdr.ScMapEntry({
|
|
308
|
-
key: xdr.ScVal.scvSymbol('signature'),
|
|
309
|
-
val: xdr.ScVal.scvBytes(signature),
|
|
310
|
-
}),
|
|
311
|
-
]))
|
|
312
|
-
|
|
313
|
-
const sim = await this.rpc.simulateTransaction(txn)
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
SorobanRpc.Api.isSimulationError(sim)
|
|
317
|
-
|| SorobanRpc.Api.isSimulationRestore(sim) // TODO handle restore flow
|
|
318
|
-
) throw sim
|
|
319
|
-
|
|
320
|
-
return SorobanRpc.assembleTransaction(txn, sim).build().toXDR()
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
public async send(txn: Transaction, fee: number = 10_000) {
|
|
324
|
-
const data = new FormData();
|
|
325
|
-
|
|
326
|
-
data.set('xdr', txn.toXDR());
|
|
327
|
-
data.set('fee', fee.toString());
|
|
328
|
-
|
|
329
|
-
const bumptxn = await fetch(this.feeBumpUrl!, {
|
|
330
|
-
method: 'POST',
|
|
331
|
-
headers: {
|
|
332
|
-
authorization: `Bearer ${this.feeBumpJwt}`,
|
|
333
|
-
},
|
|
334
|
-
body: data
|
|
335
|
-
}).then(async (res) => {
|
|
336
|
-
if (res.ok)
|
|
337
|
-
return res.text()
|
|
338
|
-
else throw await res.json()
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
return this.horizon.submitTransaction(new FeeBumpTransaction(bumptxn, this.networkPassphrase))
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
public async getData() {
|
|
345
|
-
const data: Map<string, any> = new Map()
|
|
346
|
-
|
|
347
|
-
const { val } = await this.rpc.getContractData(
|
|
348
|
-
this.wallet!.options.contractId,
|
|
349
|
-
xdr.ScVal.scvLedgerKeyContractInstance(),
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
val.contractData()
|
|
353
|
-
.val()
|
|
354
|
-
.instance()
|
|
355
|
-
.storage()
|
|
356
|
-
?.forEach((entry) => {
|
|
357
|
-
data.set(
|
|
358
|
-
scValToNative(entry.key()),
|
|
359
|
-
scValToNative(entry.val()),
|
|
360
|
-
);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
this.sudoKeyId = base64url(data.get('sudo_sig'))
|
|
364
|
-
|
|
365
|
-
return data
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/* TODO
|
|
369
|
-
- Add a getKeyInfo action to get info about a specific passkey
|
|
370
|
-
Specifically looking for name, type, etc. data so a user could grok what signer mapped to what passkey
|
|
371
|
-
@Later
|
|
372
|
-
*/
|
|
373
|
-
|
|
374
|
-
private getPublicKeyObject(attestationObject: string) {
|
|
375
|
-
const { authData } = decode(base64url.toBuffer(attestationObject));
|
|
376
|
-
const authDataUint8Array = new Uint8Array(authData);
|
|
377
|
-
const authDataView = new DataView(authDataUint8Array.buffer, 0, authDataUint8Array.length);
|
|
378
|
-
|
|
379
|
-
let offset = 0;
|
|
380
|
-
|
|
381
|
-
// RP ID Hash (32 bytes)
|
|
382
|
-
const rpIdHash = authData.slice(offset, offset + 32);
|
|
383
|
-
offset += 32;
|
|
384
|
-
|
|
385
|
-
// Flags (1 byte)
|
|
386
|
-
const flags = authDataView.getUint8(offset);
|
|
387
|
-
offset += 1;
|
|
388
|
-
|
|
389
|
-
// Sign Count (4 bytes, big endian)
|
|
390
|
-
const signCount = authDataView.getUint32(offset, false);
|
|
391
|
-
offset += 4;
|
|
392
|
-
|
|
393
|
-
// Attested Credential Data, if present
|
|
394
|
-
if (flags & 0x40) { // Checking the AT flag
|
|
395
|
-
// AAGUID (16 bytes)
|
|
396
|
-
const aaguid = authData.slice(offset, offset + 16);
|
|
397
|
-
offset += 16;
|
|
398
|
-
|
|
399
|
-
// Credential ID Length (2 bytes, big endian)
|
|
400
|
-
const credIdLength = authDataView.getUint16(offset, false);
|
|
401
|
-
offset += 2;
|
|
402
|
-
|
|
403
|
-
// Credential ID (variable length)
|
|
404
|
-
const credentialId = authData.slice(offset, offset + credIdLength);
|
|
405
|
-
offset += credIdLength;
|
|
406
|
-
|
|
407
|
-
// Credential Public Key - (77 bytes...I hope)
|
|
408
|
-
const credentialPublicKey = authData.slice(offset, offset + 77);
|
|
409
|
-
offset += 77;
|
|
410
|
-
|
|
411
|
-
// Any leftover bytes. I found some when using a YubiKey
|
|
412
|
-
const theRest = authData.slice(offset);
|
|
413
|
-
|
|
414
|
-
// Decode the credential public key to COSE
|
|
415
|
-
const publicKeyObject = new Map<string, any>(Object.entries(decode(credentialPublicKey)));
|
|
416
|
-
|
|
417
|
-
return {
|
|
418
|
-
rpIdHash,
|
|
419
|
-
flags,
|
|
420
|
-
signCount,
|
|
421
|
-
aaguid,
|
|
422
|
-
credIdLength,
|
|
423
|
-
credentialId,
|
|
424
|
-
credentialPublicKey,
|
|
425
|
-
theRest,
|
|
426
|
-
publicKeyObject
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
throw new Error("Attested credential data not present in the flags.");
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
private convertEcdsaSignatureAsnToCompact(sig: Buffer) {
|
|
434
|
-
// Define the order of the curve secp256k1
|
|
435
|
-
// https://github.com/RustCrypto/elliptic-curves/blob/master/p256/src/lib.rs#L72
|
|
436
|
-
const q = Buffer.from('ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551', 'hex')
|
|
437
|
-
|
|
438
|
-
// ASN Sequence
|
|
439
|
-
let offset = 0;
|
|
440
|
-
if (sig[offset] != 0x30) {
|
|
441
|
-
throw "signature is not a sequence";
|
|
442
|
-
}
|
|
443
|
-
offset += 1;
|
|
444
|
-
|
|
445
|
-
// ASN Sequence Byte Length
|
|
446
|
-
offset += 1;
|
|
447
|
-
|
|
448
|
-
// ASN Integer (R)
|
|
449
|
-
if (sig[offset] != 0x02) {
|
|
450
|
-
throw "first element in sequence is not an integer";
|
|
451
|
-
}
|
|
452
|
-
offset += 1;
|
|
453
|
-
|
|
454
|
-
// ASN Integer (R) Byte Length
|
|
455
|
-
const rLen = sig[offset];
|
|
456
|
-
offset += 1;
|
|
457
|
-
|
|
458
|
-
// ASN Integer (R) Byte Value
|
|
459
|
-
if (rLen >= 33) {
|
|
460
|
-
if (rLen != 33 || sig[offset] != 0x00) {
|
|
461
|
-
throw "can only handle larger than 32 byte R's that are len 33 and lead with zero";
|
|
462
|
-
}
|
|
463
|
-
offset += 1;
|
|
464
|
-
}
|
|
465
|
-
const r = sig.slice(offset, offset + 32);
|
|
466
|
-
offset += 32;
|
|
467
|
-
|
|
468
|
-
// ASN Integer (S)
|
|
469
|
-
if (sig[offset] != 0x02) {
|
|
470
|
-
throw "second element in sequence is not an integer";
|
|
471
|
-
}
|
|
472
|
-
offset += 1;
|
|
473
|
-
|
|
474
|
-
// ASN Integer (S) Byte Length
|
|
475
|
-
const sLen = sig[offset];
|
|
476
|
-
offset += 1;
|
|
477
|
-
|
|
478
|
-
// ASN Integer (S) Byte Value
|
|
479
|
-
if (sLen >= 33) {
|
|
480
|
-
if (sLen != 33 || sig[offset] != 0x00) {
|
|
481
|
-
throw "can only handle larger than 32 byte R's that are len 33 and lead with zero";
|
|
482
|
-
}
|
|
483
|
-
offset += 1;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const s = sig.slice(offset, offset + 32);
|
|
487
|
-
|
|
488
|
-
offset += 32;
|
|
489
|
-
|
|
490
|
-
let signature64: Buffer
|
|
491
|
-
|
|
492
|
-
// Force low S range
|
|
493
|
-
// https://github.com/stellar/stellar-protocol/discussions/1435#discussioncomment-8809175
|
|
494
|
-
// https://discord.com/channels/897514728459468821/1233048618571927693
|
|
495
|
-
if (bufToBigint(s) > ((bufToBigint(q) - BigInt(1)) / BigInt(2))) {
|
|
496
|
-
signature64 = Buffer.from([...r, ...Buffer.from(bigintToBuf(bufToBigint(q) - bufToBigint(s), true) as ArrayBuffer)]);
|
|
497
|
-
} else {
|
|
498
|
-
signature64 = Buffer.from([...r, ...s]);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return signature64;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function isAuthentication(value: any): value is RegistrationResponseJSON {
|
|
506
|
-
return value?.response?.attestationObject;
|
|
507
|
-
}
|
|
4
|
+
export { PasskeyBase, PasskeyKit }
|
package/src/kit.ts
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { Client as PasskeyClient } from 'passkey-kit-sdk'
|
|
2
|
+
import { Client as FactoryClient, networks } from 'passkey-factory-sdk'
|
|
3
|
+
import { Address, Networks, StrKey, hash, xdr, Transaction, SorobanRpc, Operation, scValToNative, TransactionBuilder } from '@stellar/stellar-sdk'
|
|
4
|
+
import { bufToBigint, bigintToBuf } from 'bigint-conversion'
|
|
5
|
+
import base64url from 'base64url'
|
|
6
|
+
import { startRegistration, startAuthentication } from "@simplewebauthn/browser"
|
|
7
|
+
import { decode } from 'cbor-x/decode'
|
|
8
|
+
import { Buffer } from 'buffer'
|
|
9
|
+
import { PasskeyBase } from './base'
|
|
10
|
+
|
|
11
|
+
/* TODO
|
|
12
|
+
- Clean up these params and the interface as a whole
|
|
13
|
+
Might put wallet activities and maybe factory as well into the root of the class vs buried inside this.wallet and this.factory
|
|
14
|
+
@Later
|
|
15
|
+
|
|
16
|
+
- Right now publicKey can mean a Stellar public key or a passkey public key, there should be a noted difference
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export class PasskeyKit extends PasskeyBase {
|
|
20
|
+
public keyId: string | undefined
|
|
21
|
+
public sudoKeyId: string | undefined
|
|
22
|
+
public wallet: PasskeyClient | undefined
|
|
23
|
+
public factory: FactoryClient
|
|
24
|
+
public networkPassphrase: Networks
|
|
25
|
+
public rpcUrl: string
|
|
26
|
+
public rpc: SorobanRpc.Server
|
|
27
|
+
public factoryContractId: string = networks.testnet.contractId
|
|
28
|
+
|
|
29
|
+
/* TODO
|
|
30
|
+
- Consider adding the ability to pass in a keyId and maybe even a contractId in order to preconnect to a wallet
|
|
31
|
+
If just a keyId call `connectWallet` in order to get the contractId
|
|
32
|
+
If both keyId and contractId are passed in then we can skip the connectWallet call (though we won't get the sudoKeyId in that case)
|
|
33
|
+
We don't strictly _need_ this as a dev can just call `connectWallet` after class instantiation but it might be a nice convenience
|
|
34
|
+
@Later
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
constructor(options: {
|
|
38
|
+
networkPassphrase: Networks,
|
|
39
|
+
rpcUrl: string,
|
|
40
|
+
launchtubeUrl?: string,
|
|
41
|
+
launchtubeJwt?: string,
|
|
42
|
+
/* TODO
|
|
43
|
+
- Maybe remove this? The factory should likely be baked in a bit more tightly
|
|
44
|
+
On the other hand once we have a standard interface the factory interface only uses the `deploy` method right now inside the interface
|
|
45
|
+
@No
|
|
46
|
+
*/
|
|
47
|
+
factoryContractId?: string,
|
|
48
|
+
}) {
|
|
49
|
+
const {
|
|
50
|
+
networkPassphrase,
|
|
51
|
+
rpcUrl,
|
|
52
|
+
launchtubeUrl,
|
|
53
|
+
launchtubeJwt,
|
|
54
|
+
factoryContractId
|
|
55
|
+
} = options
|
|
56
|
+
|
|
57
|
+
super({
|
|
58
|
+
launchtubeUrl,
|
|
59
|
+
launchtubeJwt,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
this.networkPassphrase = networkPassphrase
|
|
63
|
+
this.rpcUrl = rpcUrl
|
|
64
|
+
this.rpc = new SorobanRpc.Server(rpcUrl)
|
|
65
|
+
|
|
66
|
+
if (factoryContractId)
|
|
67
|
+
this.factoryContractId = factoryContractId
|
|
68
|
+
|
|
69
|
+
this.factory = new FactoryClient({
|
|
70
|
+
contractId: this.factoryContractId,
|
|
71
|
+
networkPassphrase,
|
|
72
|
+
rpcUrl
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public async createWallet(name: string, user: string) {
|
|
77
|
+
const { keyId, publicKey } = await this.createKey(name, user)
|
|
78
|
+
|
|
79
|
+
const { result, built } = await this.factory.deploy({
|
|
80
|
+
id: keyId,
|
|
81
|
+
pk: publicKey!
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const contractId = result.unwrap() as string
|
|
85
|
+
|
|
86
|
+
this.wallet = new PasskeyClient({
|
|
87
|
+
contractId,
|
|
88
|
+
networkPassphrase: this.networkPassphrase,
|
|
89
|
+
rpcUrl: this.rpcUrl
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
keyId,
|
|
94
|
+
contractId,
|
|
95
|
+
xdr: built!.toXDR() as string
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public async createKey(name: string, user: string) {
|
|
100
|
+
const startRegistrationResponse = await startRegistration({
|
|
101
|
+
challenge: base64url("stellaristhebetterblockchain"),
|
|
102
|
+
rp: {
|
|
103
|
+
// id: undefined,
|
|
104
|
+
name,
|
|
105
|
+
},
|
|
106
|
+
user: {
|
|
107
|
+
id: base64url(user),
|
|
108
|
+
name: user,
|
|
109
|
+
displayName: user,
|
|
110
|
+
},
|
|
111
|
+
authenticatorSelection: {
|
|
112
|
+
requireResidentKey: false,
|
|
113
|
+
residentKey: "preferred",
|
|
114
|
+
userVerification: "discouraged",
|
|
115
|
+
},
|
|
116
|
+
pubKeyCredParams: [{ alg: -7, type: "public-key" }],
|
|
117
|
+
attestation: "none",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (!this.keyId) {
|
|
121
|
+
this.keyId = startRegistrationResponse.id
|
|
122
|
+
|
|
123
|
+
// If there was no keyId we're likely about to deploy a new wallet so we should set the sudoKeyId
|
|
124
|
+
if (!this.sudoKeyId)
|
|
125
|
+
this.sudoKeyId = startRegistrationResponse.id
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { publicKeyObject } = this.getPublicKeyObject(startRegistrationResponse.response.attestationObject);
|
|
129
|
+
|
|
130
|
+
const publicKey = Buffer.from([
|
|
131
|
+
4, // (0x04 prefix) https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm
|
|
132
|
+
...publicKeyObject.get('-2')!,
|
|
133
|
+
...publicKeyObject.get('-3')!
|
|
134
|
+
])
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
keyId: base64url.toBuffer(startRegistrationResponse.id),
|
|
138
|
+
publicKey
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public async connectWallet(id?: string) {
|
|
143
|
+
/* TODO
|
|
144
|
+
- Support passing in a contractId as well as a keyId
|
|
145
|
+
Maybe not as we wouldn't have a keyId which could have interesting consequences
|
|
146
|
+
Also not sure what the use case would be for this where keyId wouldn't also be possible and better
|
|
147
|
+
@No
|
|
148
|
+
*/
|
|
149
|
+
|
|
150
|
+
// @ts-ignore
|
|
151
|
+
// https://github.com/stellar/js-stellar-base/issues/750
|
|
152
|
+
// if (id && StrKey.isValidContract(id)) {
|
|
153
|
+
|
|
154
|
+
// } else {
|
|
155
|
+
|
|
156
|
+
// }
|
|
157
|
+
|
|
158
|
+
const startAuthenticationResponse = id
|
|
159
|
+
? { id }
|
|
160
|
+
: await startAuthentication({
|
|
161
|
+
challenge: base64url("stellaristhebetterblockchain"),
|
|
162
|
+
// rpId: undefined,
|
|
163
|
+
userVerification: "discouraged",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (!this.keyId)
|
|
167
|
+
this.keyId = startAuthenticationResponse.id
|
|
168
|
+
|
|
169
|
+
const keyIdBuffer = base64url.toBuffer(startAuthenticationResponse.id)
|
|
170
|
+
|
|
171
|
+
// NOTE might not need this for derivation as all signers are stored in the factory and we can use that lookup as both primary and secondary
|
|
172
|
+
let contractId = StrKey.encodeContract(hash(xdr.HashIdPreimage.envelopeTypeContractId(
|
|
173
|
+
new xdr.HashIdPreimageContractId({
|
|
174
|
+
networkId: hash(Buffer.from(this.networkPassphrase, 'utf-8')),
|
|
175
|
+
contractIdPreimage: xdr.ContractIdPreimage.contractIdPreimageFromAddress(
|
|
176
|
+
new xdr.ContractIdPreimageFromAddress({
|
|
177
|
+
address: Address.fromString(this.factoryContractId).toScAddress(),
|
|
178
|
+
salt: hash(keyIdBuffer),
|
|
179
|
+
})
|
|
180
|
+
)
|
|
181
|
+
})
|
|
182
|
+
).toXDR()));
|
|
183
|
+
|
|
184
|
+
// attempt passkey id derivation
|
|
185
|
+
try {
|
|
186
|
+
await this.rpc.getContractData(contractId, xdr.ScVal.scvLedgerKeyContractInstance())
|
|
187
|
+
}
|
|
188
|
+
// if that fails look up from the factory mapper
|
|
189
|
+
catch {
|
|
190
|
+
const { val } = await this.rpc.getContractData(this.factoryContractId, xdr.ScVal.scvBytes(keyIdBuffer))
|
|
191
|
+
contractId = scValToNative(val.contractData().val())
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.wallet = new PasskeyClient({
|
|
195
|
+
contractId,
|
|
196
|
+
networkPassphrase: this.networkPassphrase,
|
|
197
|
+
rpcUrl: this.rpcUrl
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// get and set the sudo signer
|
|
201
|
+
await this.getData()
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
keyId: keyIdBuffer,
|
|
205
|
+
contractId
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* TODO
|
|
210
|
+
- Is there anything to be done with using the TS bindings `signAuthEntries` logic? Likely not but worth exploring
|
|
211
|
+
Perhaps not but maybe the js-sdk's `authorizeEntry` or `authorizeInvocation`
|
|
212
|
+
https://stellar.github.io/js-stellar-sdk/global.html#authorizeInvocation
|
|
213
|
+
*/
|
|
214
|
+
public async sign(
|
|
215
|
+
txn: Transaction | string,
|
|
216
|
+
options?: {
|
|
217
|
+
keyId?: 'any' | 'sudo' | string | Uint8Array
|
|
218
|
+
ledgersToLive?: number
|
|
219
|
+
}
|
|
220
|
+
) {
|
|
221
|
+
// Default mirrors DEFAULT_TIMEOUT (currently 5 minutes) https://github.com/stellar/js-stellar-sdk/blob/master/src/contract/utils.ts#L7
|
|
222
|
+
let { keyId, ledgersToLive = 60 } = options || {}
|
|
223
|
+
|
|
224
|
+
// Hack to ensure we don't stack fees when simulating and assembling multiple times
|
|
225
|
+
txn = TransactionBuilder.cloneFrom(new Transaction(
|
|
226
|
+
typeof txn === 'string'
|
|
227
|
+
? txn
|
|
228
|
+
: txn.toXDR(),
|
|
229
|
+
this.networkPassphrase
|
|
230
|
+
), { fee: '0' }).build()
|
|
231
|
+
|
|
232
|
+
// NOTE hard coded to sign only Soroban transactions and only and always the first auth
|
|
233
|
+
/* TODO !!!
|
|
234
|
+
- Switch to support signing multiple and specific auth entries
|
|
235
|
+
Possibly pass in auth entry vs the entire txn
|
|
236
|
+
Probably a bad idea as it would add complexity to the user side to reassemble the signed entries
|
|
237
|
+
I think we can probably look at the list of auth entries and assume which ones we want to sign
|
|
238
|
+
*/
|
|
239
|
+
const op = txn.operations[0] as Operation.InvokeHostFunction
|
|
240
|
+
const auth = op.auth![0]
|
|
241
|
+
const lastLedger = await this.rpc.getLatestLedger().then(({ sequence }) => sequence)
|
|
242
|
+
const authHash = hash(
|
|
243
|
+
xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
|
|
244
|
+
new xdr.HashIdPreimageSorobanAuthorization({
|
|
245
|
+
networkId: hash(Buffer.from(this.networkPassphrase, 'utf-8')),
|
|
246
|
+
nonce: auth.credentials().address().nonce(),
|
|
247
|
+
signatureExpirationLedger: lastLedger + ledgersToLive,
|
|
248
|
+
invocation: auth.rootInvocation()
|
|
249
|
+
})
|
|
250
|
+
).toXDR()
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
const authenticationResponse = await startAuthentication(
|
|
254
|
+
keyId === 'any'
|
|
255
|
+
|| (keyId === 'sudo' && !this.sudoKeyId)
|
|
256
|
+
|| (!keyId && !this.keyId)
|
|
257
|
+
? {
|
|
258
|
+
challenge: base64url(authHash),
|
|
259
|
+
// rpId: undefined,
|
|
260
|
+
userVerification: "discouraged",
|
|
261
|
+
}
|
|
262
|
+
: {
|
|
263
|
+
challenge: base64url(authHash),
|
|
264
|
+
// rpId: undefined,
|
|
265
|
+
allowCredentials: [
|
|
266
|
+
{
|
|
267
|
+
id: keyId === 'sudo'
|
|
268
|
+
? this.sudoKeyId!
|
|
269
|
+
: keyId instanceof Uint8Array
|
|
270
|
+
? base64url(keyId)
|
|
271
|
+
: keyId || this.keyId!,
|
|
272
|
+
type: "public-key",
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
userVerification: "discouraged",
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// set sudo if this is a sudo request
|
|
280
|
+
if (keyId === 'sudo')
|
|
281
|
+
this.sudoKeyId = authenticationResponse.id
|
|
282
|
+
|
|
283
|
+
// reset this.keyId to be the most recently used passkey
|
|
284
|
+
this.keyId = authenticationResponse.id
|
|
285
|
+
|
|
286
|
+
const signatureRaw = base64url.toBuffer(authenticationResponse.response.signature);
|
|
287
|
+
const signature = this.convertEcdsaSignatureAsnToCompact(signatureRaw);
|
|
288
|
+
const creds = auth.credentials().address();
|
|
289
|
+
|
|
290
|
+
creds.signatureExpirationLedger(lastLedger + ledgersToLive)
|
|
291
|
+
creds.signature(xdr.ScVal.scvMap([
|
|
292
|
+
new xdr.ScMapEntry({
|
|
293
|
+
key: xdr.ScVal.scvSymbol('authenticator_data'),
|
|
294
|
+
val: xdr.ScVal.scvBytes(base64url.toBuffer(authenticationResponse.response.authenticatorData)),
|
|
295
|
+
}),
|
|
296
|
+
new xdr.ScMapEntry({
|
|
297
|
+
key: xdr.ScVal.scvSymbol('client_data_json'),
|
|
298
|
+
val: xdr.ScVal.scvBytes(base64url.toBuffer(authenticationResponse.response.clientDataJSON)),
|
|
299
|
+
}),
|
|
300
|
+
new xdr.ScMapEntry({
|
|
301
|
+
key: xdr.ScVal.scvSymbol('id'),
|
|
302
|
+
val: xdr.ScVal.scvBytes(base64url.toBuffer(authenticationResponse.id)),
|
|
303
|
+
}),
|
|
304
|
+
new xdr.ScMapEntry({
|
|
305
|
+
key: xdr.ScVal.scvSymbol('signature'),
|
|
306
|
+
val: xdr.ScVal.scvBytes(signature),
|
|
307
|
+
}),
|
|
308
|
+
]))
|
|
309
|
+
|
|
310
|
+
const sim = await this.rpc.simulateTransaction(txn)
|
|
311
|
+
|
|
312
|
+
if (
|
|
313
|
+
SorobanRpc.Api.isSimulationError(sim)
|
|
314
|
+
|| SorobanRpc.Api.isSimulationRestore(sim) // TODO handle state archival
|
|
315
|
+
) throw sim
|
|
316
|
+
|
|
317
|
+
return SorobanRpc.assembleTransaction(txn, sim).build().toXDR()
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
public async getData() {
|
|
321
|
+
const data: Map<string, any> = new Map()
|
|
322
|
+
|
|
323
|
+
const { val } = await this.rpc.getContractData(
|
|
324
|
+
this.wallet!.options.contractId,
|
|
325
|
+
xdr.ScVal.scvLedgerKeyContractInstance(),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
val.contractData()
|
|
329
|
+
.val()
|
|
330
|
+
.instance()
|
|
331
|
+
.storage()
|
|
332
|
+
?.forEach((entry) => {
|
|
333
|
+
data.set(
|
|
334
|
+
scValToNative(entry.key()),
|
|
335
|
+
scValToNative(entry.val()),
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
this.sudoKeyId = base64url(data.get('sudo_sig'))
|
|
340
|
+
|
|
341
|
+
return data
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* TODO
|
|
345
|
+
- Add a getKeyInfo action to get info about a specific passkey
|
|
346
|
+
Specifically looking for name, type, etc. data so a user could grok what signer mapped to what passkey
|
|
347
|
+
@Later
|
|
348
|
+
*/
|
|
349
|
+
|
|
350
|
+
private getPublicKeyObject(attestationObject: string) {
|
|
351
|
+
const { authData } = decode(base64url.toBuffer(attestationObject));
|
|
352
|
+
const authDataUint8Array = new Uint8Array(authData);
|
|
353
|
+
const authDataView = new DataView(authDataUint8Array.buffer, 0, authDataUint8Array.length);
|
|
354
|
+
|
|
355
|
+
let offset = 0;
|
|
356
|
+
|
|
357
|
+
// RP ID Hash (32 bytes)
|
|
358
|
+
const rpIdHash = authData.slice(offset, offset + 32);
|
|
359
|
+
offset += 32;
|
|
360
|
+
|
|
361
|
+
// Flags (1 byte)
|
|
362
|
+
const flags = authDataView.getUint8(offset);
|
|
363
|
+
offset += 1;
|
|
364
|
+
|
|
365
|
+
// Sign Count (4 bytes, big endian)
|
|
366
|
+
const signCount = authDataView.getUint32(offset, false);
|
|
367
|
+
offset += 4;
|
|
368
|
+
|
|
369
|
+
// Attested Credential Data, if present
|
|
370
|
+
if (flags & 0x40) { // Checking the AT flag
|
|
371
|
+
// AAGUID (16 bytes)
|
|
372
|
+
const aaguid = authData.slice(offset, offset + 16);
|
|
373
|
+
offset += 16;
|
|
374
|
+
|
|
375
|
+
// Credential ID Length (2 bytes, big endian)
|
|
376
|
+
const credIdLength = authDataView.getUint16(offset, false);
|
|
377
|
+
offset += 2;
|
|
378
|
+
|
|
379
|
+
// Credential ID (variable length)
|
|
380
|
+
const credentialId = authData.slice(offset, offset + credIdLength);
|
|
381
|
+
offset += credIdLength;
|
|
382
|
+
|
|
383
|
+
// Credential Public Key - (77 bytes...I hope)
|
|
384
|
+
const credentialPublicKey = authData.slice(offset, offset + 77);
|
|
385
|
+
offset += 77;
|
|
386
|
+
|
|
387
|
+
// Any leftover bytes. I found some when using a YubiKey
|
|
388
|
+
const theRest = authData.slice(offset);
|
|
389
|
+
|
|
390
|
+
// Decode the credential public key to COSE
|
|
391
|
+
const publicKeyObject = new Map<string, any>(Object.entries(decode(credentialPublicKey)));
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
rpIdHash,
|
|
395
|
+
flags,
|
|
396
|
+
signCount,
|
|
397
|
+
aaguid,
|
|
398
|
+
credIdLength,
|
|
399
|
+
credentialId,
|
|
400
|
+
credentialPublicKey,
|
|
401
|
+
theRest,
|
|
402
|
+
publicKeyObject
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
throw new Error("Attested credential data not present in the flags.");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private convertEcdsaSignatureAsnToCompact(sig: Buffer) {
|
|
410
|
+
// Define the order of the curve secp256k1
|
|
411
|
+
// https://github.com/RustCrypto/elliptic-curves/blob/master/p256/src/lib.rs#L72
|
|
412
|
+
const q = Buffer.from('ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551', 'hex')
|
|
413
|
+
|
|
414
|
+
// ASN Sequence
|
|
415
|
+
let offset = 0;
|
|
416
|
+
if (sig[offset] != 0x30) {
|
|
417
|
+
throw "signature is not a sequence";
|
|
418
|
+
}
|
|
419
|
+
offset += 1;
|
|
420
|
+
|
|
421
|
+
// ASN Sequence Byte Length
|
|
422
|
+
offset += 1;
|
|
423
|
+
|
|
424
|
+
// ASN Integer (R)
|
|
425
|
+
if (sig[offset] != 0x02) {
|
|
426
|
+
throw "first element in sequence is not an integer";
|
|
427
|
+
}
|
|
428
|
+
offset += 1;
|
|
429
|
+
|
|
430
|
+
// ASN Integer (R) Byte Length
|
|
431
|
+
const rLen = sig[offset];
|
|
432
|
+
offset += 1;
|
|
433
|
+
|
|
434
|
+
// ASN Integer (R) Byte Value
|
|
435
|
+
if (rLen >= 33) {
|
|
436
|
+
if (rLen != 33 || sig[offset] != 0x00) {
|
|
437
|
+
throw "can only handle larger than 32 byte R's that are len 33 and lead with zero";
|
|
438
|
+
}
|
|
439
|
+
offset += 1;
|
|
440
|
+
}
|
|
441
|
+
const r = sig.slice(offset, offset + 32);
|
|
442
|
+
offset += 32;
|
|
443
|
+
|
|
444
|
+
// ASN Integer (S)
|
|
445
|
+
if (sig[offset] != 0x02) {
|
|
446
|
+
throw "second element in sequence is not an integer";
|
|
447
|
+
}
|
|
448
|
+
offset += 1;
|
|
449
|
+
|
|
450
|
+
// ASN Integer (S) Byte Length
|
|
451
|
+
const sLen = sig[offset];
|
|
452
|
+
offset += 1;
|
|
453
|
+
|
|
454
|
+
// ASN Integer (S) Byte Value
|
|
455
|
+
if (sLen >= 33) {
|
|
456
|
+
if (sLen != 33 || sig[offset] != 0x00) {
|
|
457
|
+
throw "can only handle larger than 32 byte R's that are len 33 and lead with zero";
|
|
458
|
+
}
|
|
459
|
+
offset += 1;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const s = sig.slice(offset, offset + 32);
|
|
463
|
+
|
|
464
|
+
offset += 32;
|
|
465
|
+
|
|
466
|
+
let signature64: Buffer
|
|
467
|
+
|
|
468
|
+
// Force low S range
|
|
469
|
+
// https://github.com/stellar/stellar-protocol/discussions/1435#discussioncomment-8809175
|
|
470
|
+
// https://discord.com/channels/897514728459468821/1233048618571927693
|
|
471
|
+
if (bufToBigint(s) > ((bufToBigint(q) - BigInt(1)) / BigInt(2))) {
|
|
472
|
+
signature64 = Buffer.from([...r, ...Buffer.from(bigintToBuf(bufToBigint(q) - bufToBigint(s), true) as ArrayBuffer)]);
|
|
473
|
+
} else {
|
|
474
|
+
signature64 = Buffer.from([...r, ...s]);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return signature64;
|
|
478
|
+
}
|
|
479
|
+
}
|
package/types/base.d.ts
ADDED
package/types/index.d.ts
CHANGED
|
@@ -1,49 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { Buffer } from 'buffer';
|
|
5
|
-
export declare class PasskeyAccount {
|
|
6
|
-
keyId: string | undefined;
|
|
7
|
-
sudoKeyId: string | undefined;
|
|
8
|
-
wallet: PasskeyClient | undefined;
|
|
9
|
-
factory: FactoryClient;
|
|
10
|
-
sequencePublicKey: string;
|
|
11
|
-
networkPassphrase: Networks;
|
|
12
|
-
horizonUrl: string;
|
|
13
|
-
horizon: Horizon.Server;
|
|
14
|
-
rpcUrl: string;
|
|
15
|
-
rpc: SorobanRpc.Server;
|
|
16
|
-
feeBumpUrl: string | undefined;
|
|
17
|
-
feeBumpJwt: string | undefined;
|
|
18
|
-
factoryContractId: string;
|
|
19
|
-
constructor(options: {
|
|
20
|
-
sequencePublicKey: string;
|
|
21
|
-
networkPassphrase: Networks;
|
|
22
|
-
horizonUrl: string;
|
|
23
|
-
rpcUrl: string;
|
|
24
|
-
feeBumpUrl?: string;
|
|
25
|
-
feeBumpJwt?: string;
|
|
26
|
-
factoryContractId?: string;
|
|
27
|
-
});
|
|
28
|
-
createWallet(name: string, user: string): Promise<{
|
|
29
|
-
keyId: Buffer;
|
|
30
|
-
contractId: string;
|
|
31
|
-
xdr: string;
|
|
32
|
-
}>;
|
|
33
|
-
createKey(name: string, user: string): Promise<{
|
|
34
|
-
keyId: Buffer;
|
|
35
|
-
publicKey: Buffer;
|
|
36
|
-
}>;
|
|
37
|
-
connectWallet(id?: string): Promise<{
|
|
38
|
-
keyId: Buffer;
|
|
39
|
-
contractId: string;
|
|
40
|
-
}>;
|
|
41
|
-
sign(txn: Transaction | string, options?: {
|
|
42
|
-
keyId?: 'any' | 'sudo' | string | Uint8Array;
|
|
43
|
-
ledgersToLive?: number;
|
|
44
|
-
}): Promise<string>;
|
|
45
|
-
send(txn: Transaction, fee?: number): Promise<Horizon.HorizonApi.SubmitTransactionResponse>;
|
|
46
|
-
getData(): Promise<Map<string, any>>;
|
|
47
|
-
private getPublicKeyObject;
|
|
48
|
-
private convertEcdsaSignatureAsnToCompact;
|
|
49
|
-
}
|
|
1
|
+
import { PasskeyBase } from "./base";
|
|
2
|
+
import { PasskeyKit } from "./kit";
|
|
3
|
+
export { PasskeyBase, PasskeyKit };
|
|
@@ -1,28 +1,22 @@
|
|
|
1
1
|
import { Client as PasskeyClient } from 'passkey-kit-sdk';
|
|
2
2
|
import { Client as FactoryClient } from 'passkey-factory-sdk';
|
|
3
|
-
import { Networks, Transaction,
|
|
3
|
+
import { Networks, Transaction, SorobanRpc } from '@stellar/stellar-sdk';
|
|
4
4
|
import { Buffer } from 'buffer';
|
|
5
|
-
|
|
5
|
+
import { PasskeyBase } from './base';
|
|
6
|
+
export declare class PasskeyKit extends PasskeyBase {
|
|
6
7
|
keyId: string | undefined;
|
|
7
8
|
sudoKeyId: string | undefined;
|
|
8
9
|
wallet: PasskeyClient | undefined;
|
|
9
10
|
factory: FactoryClient;
|
|
10
|
-
sequencePublicKey: string;
|
|
11
11
|
networkPassphrase: Networks;
|
|
12
|
-
horizonUrl: string;
|
|
13
|
-
horizon: Horizon.Server;
|
|
14
12
|
rpcUrl: string;
|
|
15
13
|
rpc: SorobanRpc.Server;
|
|
16
|
-
feeBumpUrl: string | undefined;
|
|
17
|
-
feeBumpJwt: string | undefined;
|
|
18
14
|
factoryContractId: string;
|
|
19
15
|
constructor(options: {
|
|
20
|
-
sequencePublicKey: string;
|
|
21
16
|
networkPassphrase: Networks;
|
|
22
|
-
horizonUrl: string;
|
|
23
17
|
rpcUrl: string;
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
launchtubeUrl?: string;
|
|
19
|
+
launchtubeJwt?: string;
|
|
26
20
|
factoryContractId?: string;
|
|
27
21
|
});
|
|
28
22
|
createWallet(name: string, user: string): Promise<{
|
|
@@ -42,7 +36,6 @@ export declare class PasskeyAccount {
|
|
|
42
36
|
keyId?: 'any' | 'sudo' | string | Uint8Array;
|
|
43
37
|
ledgersToLive?: number;
|
|
44
38
|
}): Promise<string>;
|
|
45
|
-
send(txn: Transaction, fee?: number): Promise<Horizon.HorizonApi.SubmitTransactionResponse>;
|
|
46
39
|
getData(): Promise<Map<string, any>>;
|
|
47
40
|
private getPublicKeyObject;
|
|
48
41
|
private convertEcdsaSignatureAsnToCompact;
|