@effectstream/wallets 0.100.0
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/README.md +1 -0
- package/package.json +30 -0
- package/src/IProvider.ts +62 -0
- package/src/algorand/algorand.ts +168 -0
- package/src/algorand/wrapper.ts +22 -0
- package/src/avail/avail.ts +81 -0
- package/src/avail/wrapper.ts +44 -0
- package/src/cardano/cardano.ts +194 -0
- package/src/cardano/wrapper.ts +34 -0
- package/src/effectstream.ts +382 -0
- package/src/evm/ethers.ts +104 -0
- package/src/evm/injected.ts +429 -0
- package/src/evm/types.ts +2 -0
- package/src/evm/wrapper-ethers.ts +49 -0
- package/src/evm/wrapper-injected.ts +103 -0
- package/src/midnight/midnight.ts +196 -0
- package/src/midnight/wrapper.ts +36 -0
- package/src/mina/mina.ts +175 -0
- package/src/mina/wrapper.ts +22 -0
- package/src/mod.ts +10 -0
- package/src/polkadot/polkadot.ts +166 -0
- package/src/polkadot/wrapper.ts +30 -0
- package/src/types.ts +61 -0
- package/src/utils.test.ts +64 -0
- package/src/utils.ts +133 -0
- package/src/wallet-modes.ts +124 -0
- package/src/wallets.ts +62 -0
- package/src/windows.ts +10 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Effectstream Wallets
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effectstream/wallets",
|
|
3
|
+
"version": "0.100.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"exports": "./src/mod.ts",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@effectstream/concise": "0.100.0",
|
|
8
|
+
"@effectstream/crypto": "0.100.0",
|
|
9
|
+
"@effectstream/event-client": "0.100.0",
|
|
10
|
+
"@effectstream/utils": "0.100.0",
|
|
11
|
+
"@aurowallet/mina-provider": "^1.0.12",
|
|
12
|
+
"@metamask/providers": "^22.1.0",
|
|
13
|
+
"@midnight-ntwrk/dapp-connector-api": "4.0.1",
|
|
14
|
+
"@perawallet/connect": "^1.4.2",
|
|
15
|
+
"@polkadot/api": "15.10.2",
|
|
16
|
+
"@polkadot/extension-dapp": "^0.58.10",
|
|
17
|
+
"@polkadot/extension-inject": "^0.58.10",
|
|
18
|
+
"@polkadot/util": "^13.4.3",
|
|
19
|
+
"@polkadot/util-crypto": "^13.4.3",
|
|
20
|
+
"@types/semver": "7.7.1",
|
|
21
|
+
"assert-never": "^1.4.0",
|
|
22
|
+
"avail-js-sdk": "^0.4.2",
|
|
23
|
+
"bech32": "^2.0.0",
|
|
24
|
+
"ethers": "^6.15.0",
|
|
25
|
+
"semver": "^7.7.3",
|
|
26
|
+
"viem": "2.37.3",
|
|
27
|
+
"web3": "^4.16.0",
|
|
28
|
+
"web3-utils": "^4.3.3"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/IProvider.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { AddressType } from "@effectstream/utils";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export type WalletOption = {
|
|
5
|
+
name: string; // name of the wallet used in APIs (as opposed to a human-friendly string)
|
|
6
|
+
displayName: string;
|
|
7
|
+
/**
|
|
8
|
+
* URI-encoded image
|
|
9
|
+
* DANGER: SVGs can contain Javascript, so these should only be rendered with <img> tags
|
|
10
|
+
*
|
|
11
|
+
* Note: not every wallet type has an image (ex: locally generated keypairs)
|
|
12
|
+
* Example values:
|
|
13
|
+
* data:image/svg+xml,...
|
|
14
|
+
* data:image/png;base64,...
|
|
15
|
+
*/
|
|
16
|
+
icon?: undefined | string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ActiveConnection<T> = {
|
|
20
|
+
metadata: WalletOption;
|
|
21
|
+
api: T;
|
|
22
|
+
};
|
|
23
|
+
export type ConnectionOption<T> = {
|
|
24
|
+
metadata: WalletOption;
|
|
25
|
+
api: () => Promise<T>;
|
|
26
|
+
};
|
|
27
|
+
export async function optionToActive<T>(option: ConnectionOption<T>): Promise<ActiveConnection<T>> {
|
|
28
|
+
const connection = {
|
|
29
|
+
metadata: option.metadata,
|
|
30
|
+
api: await option.api(),
|
|
31
|
+
};
|
|
32
|
+
return connection;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface IConnector<T> {
|
|
36
|
+
getProvider(): undefined | IProvider<T>;
|
|
37
|
+
getOrThrowProvider(): IProvider<T>;
|
|
38
|
+
isConnected(): boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface IInjectedConnector<T> {
|
|
42
|
+
/** connect while letting Paima pick all the connection logic for you */
|
|
43
|
+
connectSimple(): Promise<IProvider<T>>;
|
|
44
|
+
/** connect to an explicit API that Paima supports */
|
|
45
|
+
connectNamed(name: string): Promise<IProvider<T>>;
|
|
46
|
+
/** connect to an API that you've initialized yourself externally */
|
|
47
|
+
connectExternal(conn: ActiveConnection<T>): Promise<IProvider<T>>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Exported to package
|
|
51
|
+
export type AddressAndType = {
|
|
52
|
+
type: AddressType;
|
|
53
|
+
address: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type UserSignature = string;
|
|
57
|
+
|
|
58
|
+
export interface IProvider<T> {
|
|
59
|
+
getConnection(): ActiveConnection<T>;
|
|
60
|
+
signMessage(message: string): Promise<UserSignature>;
|
|
61
|
+
getAddress(): AddressAndType;
|
|
62
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { PeraWalletConnect } from "@perawallet/connect";
|
|
2
|
+
import { AddressType, uint8ArrayToHexString } from "@effectstream/utils";
|
|
3
|
+
import {
|
|
4
|
+
type IConnector,
|
|
5
|
+
type IInjectedConnector,
|
|
6
|
+
type ConnectionOption,
|
|
7
|
+
optionToActive,
|
|
8
|
+
type ActiveConnection,
|
|
9
|
+
type IProvider,
|
|
10
|
+
type AddressAndType,
|
|
11
|
+
type UserSignature,
|
|
12
|
+
} from "../IProvider.ts";
|
|
13
|
+
import { getWindow } from "../windows.ts";
|
|
14
|
+
import { CryptoManager } from "@effectstream/crypto";
|
|
15
|
+
|
|
16
|
+
export type AlgorandApi = PeraWalletConnect;
|
|
17
|
+
export type AlgorandAddress = string;
|
|
18
|
+
|
|
19
|
+
export class AlgorandConnector
|
|
20
|
+
implements IConnector<AlgorandApi>, IInjectedConnector<AlgorandApi>
|
|
21
|
+
{
|
|
22
|
+
private provider: AlgorandProvider | undefined;
|
|
23
|
+
private static INSTANCE: undefined | AlgorandConnector = undefined;
|
|
24
|
+
|
|
25
|
+
static getWalletOptions(): ConnectionOption<AlgorandApi>[] {
|
|
26
|
+
// Algorand has no standard for wallet discovery
|
|
27
|
+
// The closest that exists is ARC11 (https://arc.algorand.foundation/ARCs/arc-0011)
|
|
28
|
+
// but it doesn't give any information about which wallet is injected
|
|
29
|
+
// and, similar to window.ethereum, has wallets overriding each other
|
|
30
|
+
// and Pera wallet doesn't even use this standard
|
|
31
|
+
// instead, the best we can do is check if Pera injected its UI component in the window
|
|
32
|
+
if (
|
|
33
|
+
(getWindow() as any)?.customElements.get("pera-wallet-connect-modal") ==
|
|
34
|
+
null
|
|
35
|
+
) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
metadata: {
|
|
41
|
+
name: "pera",
|
|
42
|
+
displayName: "Pera Wallet",
|
|
43
|
+
},
|
|
44
|
+
api: async (): Promise<AlgorandApi> => {
|
|
45
|
+
// const { PeraWalletConnect } = await import("@perawallet/connect");
|
|
46
|
+
const peraWallet = new PeraWalletConnect();
|
|
47
|
+
return peraWallet;
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static instance(): AlgorandConnector {
|
|
54
|
+
if (AlgorandConnector.INSTANCE == null) {
|
|
55
|
+
const newInstance = new AlgorandConnector();
|
|
56
|
+
AlgorandConnector.INSTANCE = newInstance;
|
|
57
|
+
}
|
|
58
|
+
return AlgorandConnector.INSTANCE;
|
|
59
|
+
}
|
|
60
|
+
connectSimple = async (): Promise<AlgorandProvider> => {
|
|
61
|
+
if (this.provider != null) {
|
|
62
|
+
return this.provider;
|
|
63
|
+
}
|
|
64
|
+
const options = AlgorandConnector.getWalletOptions();
|
|
65
|
+
if (options.length === 0) {
|
|
66
|
+
throw new Error(`No Algorand wallet found`);
|
|
67
|
+
}
|
|
68
|
+
return await this.connectExternal(await optionToActive(options[0]));
|
|
69
|
+
};
|
|
70
|
+
connectExternal = async (
|
|
71
|
+
conn: ActiveConnection<AlgorandApi>
|
|
72
|
+
): Promise<AlgorandProvider> => {
|
|
73
|
+
if (this.provider?.getConnection().metadata?.name === conn.metadata.name) {
|
|
74
|
+
return this.provider;
|
|
75
|
+
}
|
|
76
|
+
this.provider = await AlgorandProvider.init(conn);
|
|
77
|
+
return this.provider;
|
|
78
|
+
};
|
|
79
|
+
connectNamed = async (name: string): Promise<AlgorandProvider> => {
|
|
80
|
+
if (this.provider?.getConnection().metadata?.name === name) {
|
|
81
|
+
return this.provider;
|
|
82
|
+
}
|
|
83
|
+
const provider = AlgorandConnector.getWalletOptions().find(
|
|
84
|
+
(entry) => entry.metadata.name === name
|
|
85
|
+
);
|
|
86
|
+
if (provider == null) {
|
|
87
|
+
throw new Error(`AlgorandProvider: unsupported connection type ${name}`);
|
|
88
|
+
}
|
|
89
|
+
return await this.connectExternal(await optionToActive(provider));
|
|
90
|
+
};
|
|
91
|
+
getProvider = (): undefined | AlgorandProvider => {
|
|
92
|
+
return this.provider;
|
|
93
|
+
};
|
|
94
|
+
getOrThrowProvider = (): AlgorandProvider => {
|
|
95
|
+
if (this.provider == null) {
|
|
96
|
+
throw new Error(`AlgorandConnector provider isn't initialized yet`);
|
|
97
|
+
}
|
|
98
|
+
return this.provider;
|
|
99
|
+
};
|
|
100
|
+
isConnected = (): boolean => {
|
|
101
|
+
return this.provider != null;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export class AlgorandProvider implements IProvider<AlgorandApi> {
|
|
106
|
+
constructor(
|
|
107
|
+
private readonly conn: ActiveConnection<AlgorandApi>,
|
|
108
|
+
readonly address: AlgorandAddress
|
|
109
|
+
) {}
|
|
110
|
+
static init = async (
|
|
111
|
+
conn: ActiveConnection<AlgorandApi>
|
|
112
|
+
): Promise<AlgorandProvider> => {
|
|
113
|
+
let newAccounts: string[] = [];
|
|
114
|
+
// conn.api.isConnected starts as false, even if connected
|
|
115
|
+
// if connected and connected() is called, then an error is thrown
|
|
116
|
+
try {
|
|
117
|
+
newAccounts = await conn.api.reconnectSession();
|
|
118
|
+
if ( newAccounts.length < 1) {
|
|
119
|
+
throw new Error("[peraLogin] no addresses returned!");
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
newAccounts = await conn.api.connect();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (newAccounts.length < 1) {
|
|
126
|
+
throw new Error("[peraLogin] no addresses returned!");
|
|
127
|
+
}
|
|
128
|
+
return new AlgorandProvider(conn, newAccounts[0]);
|
|
129
|
+
};
|
|
130
|
+
getConnection = (): ActiveConnection<AlgorandApi> => {
|
|
131
|
+
return this.conn;
|
|
132
|
+
};
|
|
133
|
+
getAddress = (): AddressAndType => {
|
|
134
|
+
return {
|
|
135
|
+
type: AddressType.ALGORAND,
|
|
136
|
+
address: this.address,
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
signMessage = async (message: string): Promise<UserSignature> => {
|
|
140
|
+
const txn = await CryptoManager.Algorand().buildAlgorandTransaction(
|
|
141
|
+
this.getAddress().address,
|
|
142
|
+
message
|
|
143
|
+
);
|
|
144
|
+
const signerTx = {
|
|
145
|
+
txn,
|
|
146
|
+
signers: [this.getAddress().address],
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const signedTxs = await this.conn.api.signTransaction(
|
|
150
|
+
[[signerTx]],
|
|
151
|
+
this.getAddress().address
|
|
152
|
+
);
|
|
153
|
+
if (signedTxs.length !== 1) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`[signMessageAlgorand] invalid number of signatures returned: ${signedTxs.length}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const signedTx = await CryptoManager.Algorand().decodeSignedTransaction(
|
|
159
|
+
signedTxs[0]
|
|
160
|
+
);
|
|
161
|
+
const signature = signedTx.sig;
|
|
162
|
+
if (!signature) {
|
|
163
|
+
throw new Error(`[signMessageAlgorand] signature missing in signed Tx`);
|
|
164
|
+
}
|
|
165
|
+
const hexSignature = uint8ArrayToHexString(signature);
|
|
166
|
+
return hexSignature;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Result } from "@effectstream/utils";
|
|
2
|
+
import type { IProvider } from "../IProvider.ts";
|
|
3
|
+
import type { WalletMode, ApiForMode } from "../utils.ts";
|
|
4
|
+
import { type LoginInfoMap, connectInjected } from "../wallet-modes.ts";
|
|
5
|
+
import { AlgorandConnector } from "./algorand.ts";
|
|
6
|
+
|
|
7
|
+
export async function algorandLoginWrapper(
|
|
8
|
+
loginInfo: LoginInfoMap[WalletMode.Algorand]
|
|
9
|
+
): Promise<Result<IProvider<ApiForMode<WalletMode.Algorand>>>> {
|
|
10
|
+
const loginResult = await connectInjected(
|
|
11
|
+
"algorandLoginWrapper",
|
|
12
|
+
loginInfo,
|
|
13
|
+
AlgorandConnector.instance()
|
|
14
|
+
);
|
|
15
|
+
if (loginResult.success === false) {
|
|
16
|
+
return loginResult;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
success: true,
|
|
20
|
+
result: loginResult.result,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { utf8ToHex } from 'web3-utils';
|
|
3
|
+
import type { ApiPromise } from "avail-js-sdk";
|
|
4
|
+
import { AddressType } from "@effectstream/utils";
|
|
5
|
+
import type { IProvider, ActiveConnection, AddressAndType, UserSignature } from "../IProvider.ts";
|
|
6
|
+
type PolkadotAddress = string;
|
|
7
|
+
import { u8aToHex } from "@polkadot/util";
|
|
8
|
+
|
|
9
|
+
// Problematic dependency for deno-fresh
|
|
10
|
+
import type { Keyring } from "avail-js-sdk";
|
|
11
|
+
|
|
12
|
+
// Problematic dependency for deno-vite
|
|
13
|
+
// import type { Keyring } from "@polkadot/api";
|
|
14
|
+
export type AvailJsApi = { rpc: ApiPromise; keyring: Keyring };
|
|
15
|
+
|
|
16
|
+
export class AvailConnector {
|
|
17
|
+
private provider: AvailJsProvider | undefined;
|
|
18
|
+
private static INSTANCE: undefined | AvailConnector = undefined;
|
|
19
|
+
|
|
20
|
+
static instance(): AvailConnector {
|
|
21
|
+
if (AvailConnector.INSTANCE == null) {
|
|
22
|
+
const newInstance = new AvailConnector();
|
|
23
|
+
AvailConnector.INSTANCE = newInstance;
|
|
24
|
+
}
|
|
25
|
+
return AvailConnector.INSTANCE;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
connectExternal = async (conn: AvailJsApi): Promise<AvailJsProvider> => {
|
|
29
|
+
this.provider = await AvailJsProvider.init(conn);
|
|
30
|
+
return this.provider;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
getProvider = (): undefined | AvailJsProvider => {
|
|
34
|
+
return this.provider;
|
|
35
|
+
};
|
|
36
|
+
getOrThrowProvider = (): AvailJsProvider => {
|
|
37
|
+
if (this.provider == null) {
|
|
38
|
+
throw new Error(`AvailJsConnector not initialized yet`);
|
|
39
|
+
}
|
|
40
|
+
return this.provider;
|
|
41
|
+
};
|
|
42
|
+
isConnected = (): boolean => {
|
|
43
|
+
return this.provider != null;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class AvailJsProvider implements IProvider<AvailJsApi> {
|
|
48
|
+
constructor(
|
|
49
|
+
private readonly conn: ActiveConnection<AvailJsApi>,
|
|
50
|
+
readonly address: PolkadotAddress
|
|
51
|
+
) {}
|
|
52
|
+
|
|
53
|
+
static init = async (api: AvailJsApi): Promise<AvailJsProvider> => {
|
|
54
|
+
const pair = api.keyring.getPairs()[0];
|
|
55
|
+
|
|
56
|
+
const conn = {
|
|
57
|
+
api: api,
|
|
58
|
+
metadata: {
|
|
59
|
+
name: 'AvailJs',
|
|
60
|
+
displayName: 'AvailJs',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
return new AvailJsProvider(conn, pair.address);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
getConnection = (): ActiveConnection<AvailJsApi> => {
|
|
67
|
+
return this.conn;
|
|
68
|
+
};
|
|
69
|
+
getAddress = (): AddressAndType => {
|
|
70
|
+
return {
|
|
71
|
+
type: AddressType.AVAIL,
|
|
72
|
+
address: this.address,
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
signMessage = async (message: string): Promise<UserSignature> => {
|
|
76
|
+
const hexMessage = utf8ToHex(message);
|
|
77
|
+
const buffer = Buffer.from(hexMessage.slice(2), 'hex');
|
|
78
|
+
const signature = this.conn.api.keyring.getPairs()[0].sign(buffer);
|
|
79
|
+
return u8aToHex(signature);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Result } from "@effectstream/utils";
|
|
2
|
+
import type { IProvider } from "../IProvider.ts";
|
|
3
|
+
import type { WalletMode, ApiForMode } from "../utils.ts";
|
|
4
|
+
import type { LoginInfoMap } from "../wallet-modes.ts";
|
|
5
|
+
import { type AvailJsApi, AvailConnector } from "./avail.ts";
|
|
6
|
+
|
|
7
|
+
// Problematic dependency for deno-fresh
|
|
8
|
+
import { Keyring } from "avail-js-sdk";
|
|
9
|
+
|
|
10
|
+
// Problematic dependency for deno-vite
|
|
11
|
+
// import { Keyring } from "@polkadot/api";
|
|
12
|
+
|
|
13
|
+
async function connectWallet(
|
|
14
|
+
loginInfo: LoginInfoMap[WalletMode.AvailJs]
|
|
15
|
+
): Promise<Result<IProvider<AvailJsApi>>> {
|
|
16
|
+
try {
|
|
17
|
+
console.log(`availJsLoginWrapper: Attempting to log into ${loginInfo.connection.metadata.name}`);
|
|
18
|
+
const keyring = new Keyring({ type: 'sr25519' });
|
|
19
|
+
keyring.addFromUri(loginInfo.seed);
|
|
20
|
+
|
|
21
|
+
const provider = await AvailConnector.instance().connectExternal({
|
|
22
|
+
rpc: loginInfo.connection.api,
|
|
23
|
+
keyring: keyring,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
success: true,
|
|
28
|
+
result: provider,
|
|
29
|
+
};
|
|
30
|
+
} catch (err: any) {
|
|
31
|
+
console.log(`availJsLoginWrapper: Error while logging into wallet name}`);
|
|
32
|
+
return {
|
|
33
|
+
success: false,
|
|
34
|
+
errorMessage: err.message ?? String(err),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function availJsLoginWrapper(
|
|
40
|
+
loginInfo: LoginInfoMap[WalletMode.AvailJs]
|
|
41
|
+
): Promise<Result<IProvider<ApiForMode<WalletMode.AvailJs>>>> {
|
|
42
|
+
const loginResult = await connectWallet(loginInfo);
|
|
43
|
+
return loginResult;
|
|
44
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { bech32 } from "bech32";
|
|
2
|
+
import { hexStringToUint8Array, AddressType } from "@effectstream/utils";
|
|
3
|
+
import {
|
|
4
|
+
type IConnector,
|
|
5
|
+
type IInjectedConnector,
|
|
6
|
+
type ConnectionOption,
|
|
7
|
+
optionToActive,
|
|
8
|
+
type ActiveConnection,
|
|
9
|
+
type IProvider,
|
|
10
|
+
type AddressAndType,
|
|
11
|
+
type UserSignature,
|
|
12
|
+
} from "../IProvider.ts";
|
|
13
|
+
import { getWindow } from "../windows.ts";
|
|
14
|
+
import { utf8ToHex } from "web3-utils";
|
|
15
|
+
|
|
16
|
+
// TODO: proper type definitions for CIP30
|
|
17
|
+
export type CardanoApi = any;
|
|
18
|
+
/** bech32 */
|
|
19
|
+
export type CardanoAddress = {
|
|
20
|
+
bech32: string;
|
|
21
|
+
hex: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// JSR Error: modifying global types is not allowed
|
|
25
|
+
// declare global {
|
|
26
|
+
// interface Window {
|
|
27
|
+
// cardano?: Record<string, { name?: string; enable?: () => Promise<CardanoApi> }>;
|
|
28
|
+
// }
|
|
29
|
+
// }
|
|
30
|
+
|
|
31
|
+
// 4 helpers to convert hex addresses to bech32
|
|
32
|
+
const NETWORK_TAG_MAINNET = '1';
|
|
33
|
+
const NETWORK_TAG_TESTNET = '0';
|
|
34
|
+
const PREFIX_MAINNET = 'addr';
|
|
35
|
+
const PREFIX_TESTNET = 'addr_test';
|
|
36
|
+
|
|
37
|
+
export class CardanoConnector implements IConnector<CardanoApi>, IInjectedConnector<CardanoApi> {
|
|
38
|
+
private provider: CardanoProvider | undefined;
|
|
39
|
+
private static INSTANCE: undefined | CardanoConnector = undefined;
|
|
40
|
+
|
|
41
|
+
static getWalletOptions(): ConnectionOption<CardanoApi>[] {
|
|
42
|
+
const cardanoApi: Record<string, { name?: string; enable?: () => Promise<CardanoApi> }> = (getWindow() as any)?.cardano;
|
|
43
|
+
if (cardanoApi == null) return [];
|
|
44
|
+
|
|
45
|
+
const options = Object.entries(cardanoApi).reduce((options, [key, info]) => {
|
|
46
|
+
if (info.name != null && info.enable != null) {
|
|
47
|
+
options.push({
|
|
48
|
+
metadata: {
|
|
49
|
+
name: key,
|
|
50
|
+
displayName: info.name,
|
|
51
|
+
icon: 'icon' in info ? (info.icon as string) : undefined,
|
|
52
|
+
},
|
|
53
|
+
api: info.enable,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return options;
|
|
57
|
+
}, [] as ConnectionOption<CardanoApi>[]);
|
|
58
|
+
return options;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static instance(): CardanoConnector {
|
|
62
|
+
if (CardanoConnector.INSTANCE == null) {
|
|
63
|
+
const newInstance = new CardanoConnector();
|
|
64
|
+
CardanoConnector.INSTANCE = newInstance;
|
|
65
|
+
}
|
|
66
|
+
return CardanoConnector.INSTANCE;
|
|
67
|
+
}
|
|
68
|
+
connectSimple = async (): Promise<CardanoProvider> => {
|
|
69
|
+
if (this.provider != null) {
|
|
70
|
+
return this.provider;
|
|
71
|
+
}
|
|
72
|
+
const options = CardanoConnector.getWalletOptions();
|
|
73
|
+
if (options.length === 0) {
|
|
74
|
+
throw new Error(`No Cardano wallet found`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// flint has some custom support for Paima, so best to return this one first if it exists
|
|
78
|
+
const flintWallet = options.find(option => option.metadata.name === 'flint');
|
|
79
|
+
if (flintWallet != null) {
|
|
80
|
+
return await this.connectExternal(await optionToActive(flintWallet));
|
|
81
|
+
}
|
|
82
|
+
return await this.connectExternal(await optionToActive(options[0]));
|
|
83
|
+
};
|
|
84
|
+
connectNamed = async (name: string): Promise<CardanoProvider> => {
|
|
85
|
+
if (this.provider?.getConnection().metadata?.name === name) {
|
|
86
|
+
return this.provider;
|
|
87
|
+
}
|
|
88
|
+
const provider = CardanoConnector.getWalletOptions().find(
|
|
89
|
+
entry => entry.metadata.name === name
|
|
90
|
+
);
|
|
91
|
+
if (provider == null) {
|
|
92
|
+
throw new Error(`Cardano wallet ${name} not found`);
|
|
93
|
+
}
|
|
94
|
+
return await this.connectExternal(await optionToActive(provider));
|
|
95
|
+
};
|
|
96
|
+
connectExternal = async (
|
|
97
|
+
conn: ActiveConnection<CardanoApi>
|
|
98
|
+
): Promise<CardanoProvider> => {
|
|
99
|
+
if (this.provider?.getConnection().metadata?.name === conn.metadata.name) {
|
|
100
|
+
return this.provider;
|
|
101
|
+
}
|
|
102
|
+
this.provider = await CardanoProvider.init(conn);
|
|
103
|
+
return this.provider;
|
|
104
|
+
};
|
|
105
|
+
getProvider = (): undefined | CardanoProvider => {
|
|
106
|
+
return this.provider;
|
|
107
|
+
};
|
|
108
|
+
getOrThrowProvider = (): CardanoProvider => {
|
|
109
|
+
if (this.provider == null) {
|
|
110
|
+
throw new Error(`CardanoConnector provider isn't initialized yet`);
|
|
111
|
+
}
|
|
112
|
+
return this.provider;
|
|
113
|
+
};
|
|
114
|
+
isConnected = (): boolean => {
|
|
115
|
+
return this.provider != null;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function pickBech32Prefix(hexAddress: string): string {
|
|
120
|
+
if (hexAddress.length < 2) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`[cardanoLoginSpecific] Invalid address returned from wallet: ${hexAddress}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const networkTag = hexAddress.at(1);
|
|
126
|
+
switch (networkTag) {
|
|
127
|
+
case NETWORK_TAG_MAINNET:
|
|
128
|
+
return PREFIX_MAINNET;
|
|
129
|
+
case NETWORK_TAG_TESTNET:
|
|
130
|
+
return PREFIX_TESTNET;
|
|
131
|
+
default:
|
|
132
|
+
throw new Error(
|
|
133
|
+
`[cardanoLoginSpecific] Invalid network tag in address ${hexAddress}`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export class CardanoProvider implements IProvider<CardanoApi> {
|
|
139
|
+
constructor(
|
|
140
|
+
private readonly conn: ActiveConnection<CardanoApi>,
|
|
141
|
+
readonly address: CardanoAddress
|
|
142
|
+
) {}
|
|
143
|
+
|
|
144
|
+
static init = async (
|
|
145
|
+
conn: ActiveConnection<CardanoApi>
|
|
146
|
+
): Promise<CardanoProvider> => {
|
|
147
|
+
const hexAddress = await CardanoProvider.fetchAddress(conn);
|
|
148
|
+
const prefix = pickBech32Prefix(hexAddress);
|
|
149
|
+
const words = bech32.toWords(hexStringToUint8Array(hexAddress));
|
|
150
|
+
const userAddress = bech32.encode(prefix, words, 200);
|
|
151
|
+
|
|
152
|
+
return new CardanoProvider(conn, {
|
|
153
|
+
bech32: userAddress,
|
|
154
|
+
hex: hexAddress,
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
getConnection = (): ActiveConnection<CardanoApi> => {
|
|
158
|
+
return this.conn;
|
|
159
|
+
};
|
|
160
|
+
getAddress = (): AddressAndType => {
|
|
161
|
+
return {
|
|
162
|
+
type: AddressType.CARDANO,
|
|
163
|
+
address: this.address.bech32,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
signMessage = async (message: string): Promise<UserSignature> => {
|
|
167
|
+
const hexMessage = utf8ToHex(message).slice(2);
|
|
168
|
+
const address = (this.conn.metadata.name === 'nami' || this.conn.metadata.name === 'subwallet') ? this.address.hex : this.address.bech32;
|
|
169
|
+
const { signature, key } = await this.conn.api.signData(address, hexMessage);
|
|
170
|
+
return `${signature}+${key}`;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
static fetchAddress = async (conn: ActiveConnection<CardanoApi>): Promise<string> => {
|
|
174
|
+
try {
|
|
175
|
+
const addresses = await conn.api.getUsedAddresses();
|
|
176
|
+
if (addresses.length > 0) {
|
|
177
|
+
return addresses[0];
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.log('[pickCardanoAddress] error calling getUsedAddresses:', err);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const unusedAddresses = await conn.api.getUnusedAddresses();
|
|
185
|
+
if (unusedAddresses.length > 0) {
|
|
186
|
+
return unusedAddresses[0];
|
|
187
|
+
}
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.log('[pickCardanoAddress] error calling getUnusedAddresses:', err);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
throw new Error('[pickCardanoAddress] no used or unused addresses');
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Result } from "@effectstream/utils";
|
|
2
|
+
import type { IProvider } from "../IProvider.ts";
|
|
3
|
+
import type { WalletMode, ApiForMode } from "../utils.ts";
|
|
4
|
+
import { type LoginInfoMap, connectInjected } from "../wallet-modes.ts";
|
|
5
|
+
import { CardanoConnector } from "./cardano.ts";
|
|
6
|
+
|
|
7
|
+
export async function checkCardanoWalletStatus(): Promise<Result<string>> {
|
|
8
|
+
const provider = CardanoConnector.instance().getProvider();
|
|
9
|
+
if (provider == null) {
|
|
10
|
+
return {
|
|
11
|
+
success: false,
|
|
12
|
+
errorMessage: "No Cardano wallet found",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return { success: true, result: "OK" };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function cardanoLoginWrapper(
|
|
20
|
+
loginInfo: LoginInfoMap[WalletMode.Cardano]
|
|
21
|
+
): Promise<Result<IProvider<ApiForMode<WalletMode.Cardano>>>> {
|
|
22
|
+
const loginResult = await connectInjected(
|
|
23
|
+
"cardanoLoginWrapper",
|
|
24
|
+
loginInfo,
|
|
25
|
+
CardanoConnector.instance()
|
|
26
|
+
);
|
|
27
|
+
if (loginResult.success === false) {
|
|
28
|
+
return loginResult;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
success: true,
|
|
32
|
+
result: loginResult.result,
|
|
33
|
+
};
|
|
34
|
+
}
|