Skip to content

Commit 8aa67e8

Browse files
committed
feat: bring WalletManager as standard inside core library
1 parent b67dbba commit 8aa67e8

File tree

3 files changed

+340
-0
lines changed

3 files changed

+340
-0
lines changed

packages/core/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,5 @@ export {
8484
} from "./lib/helpers";
8585

8686
export { translate, allowOnlyLanguage } from "./lib/translate/translate";
87+
88+
export { WalletManager } from "./lib/wallet-manager";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { WalletManager } from "./wallet-manager";
2+
import { setupWalletSelector } from "./wallet-selector";
3+
import type {
4+
WalletSelector,
5+
WalletSelectorParams,
6+
} from "./wallet-selector.types";
7+
8+
// Mock implementations for required modules
9+
const _state: Record<string, string> = {};
10+
11+
global.localStorage = {
12+
getItem: jest.fn((key) => _state[key] || null),
13+
setItem: jest.fn((key, value) => {
14+
_state[key] = value;
15+
}),
16+
removeItem: jest.fn((key) => {
17+
delete _state[key];
18+
}),
19+
clear: jest.fn(() => {
20+
for (const key in _state) {
21+
delete _state[key];
22+
}
23+
}),
24+
get length() {
25+
return Object.keys(_state).length;
26+
},
27+
key: jest.fn((index) => Object.keys(_state)[index] || null),
28+
};
29+
30+
jest.mock("./wallet-selector", () => {
31+
const originalModule = jest.requireActual("./wallet-selector");
32+
return {
33+
...originalModule,
34+
setupWalletSelector: jest.fn(),
35+
};
36+
});
37+
38+
describe("WalletManager", () => {
39+
let isSelectorResolved: boolean;
40+
41+
beforeEach(() => {
42+
jest.clearAllMocks();
43+
44+
isSelectorResolved = false;
45+
});
46+
47+
it("waits for selector to be resolved before getAccountId returns a result", async () => {
48+
const mockedSetupWalletSelector =
49+
setupWalletSelector as jest.MockedFunction<typeof setupWalletSelector>;
50+
51+
mockedSetupWalletSelector.mockImplementationOnce(async () => {
52+
await new Promise((res) => setTimeout(res, 100));
53+
54+
isSelectorResolved = true;
55+
56+
return {
57+
isSignedIn() {
58+
return true;
59+
},
60+
store: {
61+
getState() {
62+
return { accounts: [{ accountId: "test" }] };
63+
},
64+
},
65+
} as WalletSelector;
66+
});
67+
68+
const wallet = new WalletManager({} as WalletSelectorParams);
69+
expect(isSelectorResolved).toBe(false);
70+
71+
const accountId = await wallet.getAccountId();
72+
73+
expect(isSelectorResolved).toBe(true);
74+
expect(accountId).toBe("test");
75+
});
76+
});
+262
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
import { providers, utils } from "near-api-js";
2+
import type { WalletSelectorState } from "./store.types";
3+
import { setupWalletSelector } from "./wallet-selector";
4+
import type {
5+
WalletSelector,
6+
WalletSelectorParams,
7+
} from "./wallet-selector.types";
8+
import type { FinalExecutionOutcome } from "near-api-js/lib/providers";
9+
import type { Transaction } from "./wallet";
10+
import type {
11+
AccessKeyList,
12+
AccessKeyView,
13+
AccountView,
14+
CodeResult,
15+
RpcQueryRequest,
16+
ViewStateResult,
17+
} from "near-api-js/lib/providers/provider";
18+
19+
type OnAccountChange = (account: string) => void;
20+
21+
const THIRTY_TGAS = "30000000000000";
22+
const NO_DEPOSIT = "0";
23+
24+
export class WalletManager {
25+
public readonly selector: Promise<WalletSelector>;
26+
27+
constructor(public readonly params: WalletSelectorParams) {
28+
this.selector = setupWalletSelector(this.params);
29+
}
30+
31+
public async subscribeOnAccountChange(
32+
onAccountChangeFn: OnAccountChange
33+
): Promise<void> {
34+
const selector = await this.selector;
35+
36+
selector.store.observable.subscribe(async (state: WalletSelectorState) => {
37+
const signedAccount = state?.accounts.find(
38+
(account) => account.active
39+
)?.accountId;
40+
41+
onAccountChangeFn(signedAccount || "");
42+
});
43+
}
44+
45+
/**
46+
*
47+
* @returns {string} empty string if not signed in, otherwise Account ID
48+
*/
49+
public async getAccountId(): Promise<string> {
50+
const selector = await this.selector;
51+
52+
const isSignedIn = selector.isSignedIn();
53+
54+
if (!isSignedIn) {
55+
return "";
56+
}
57+
58+
return selector.store.getState().accounts[0].accountId;
59+
}
60+
61+
public signIn = async (
62+
contractId: string,
63+
methodNames?: Array<string>
64+
): Promise<void> => {
65+
const selector = await this.selector;
66+
const wallet = await selector.wallet();
67+
68+
await wallet.signIn({
69+
contractId: contractId,
70+
methodNames: methodNames,
71+
// required for hardware wallets
72+
accounts: [],
73+
});
74+
};
75+
76+
public async signOut(): Promise<void> {
77+
const selector = await this.selector;
78+
const wallet = await selector.wallet();
79+
80+
await wallet.signOut();
81+
}
82+
83+
public async viewMethod(contractId: string, method: string, args = {}) {
84+
const selector = await this.selector;
85+
const { network } = selector.options;
86+
const provider = new providers.JsonRpcProvider({ url: network.nodeUrl });
87+
88+
const request: RpcQueryRequest = {
89+
request_type: "call_function",
90+
account_id: contractId,
91+
method_name: method,
92+
args_base64: Buffer.from(JSON.stringify(args)).toString("base64"),
93+
finality: "optimistic",
94+
};
95+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
96+
const response = await provider.query<any>(request);
97+
98+
return JSON.parse(Buffer.from(response.result).toString());
99+
}
100+
101+
public async callMethod(
102+
contractId: string,
103+
method: string,
104+
args = {},
105+
gas = THIRTY_TGAS,
106+
deposit = NO_DEPOSIT
107+
) {
108+
const selector = await this.selector;
109+
const wallet = await selector.wallet();
110+
111+
const outcome = await wallet.signAndSendTransaction({
112+
receiverId: contractId,
113+
actions: [
114+
{
115+
type: "FunctionCall",
116+
params: {
117+
methodName: method,
118+
args,
119+
gas,
120+
deposit,
121+
},
122+
},
123+
],
124+
});
125+
126+
return providers.getTransactionLastResult(outcome as FinalExecutionOutcome);
127+
}
128+
129+
public async getTransactionResult(
130+
txHash: string | Uint8Array,
131+
signerAccountId: string
132+
) {
133+
const selector = await this.selector;
134+
const { network } = selector.options;
135+
const provider = new providers.JsonRpcProvider({ url: network.nodeUrl });
136+
137+
const transaction = await provider.txStatus(txHash, signerAccountId);
138+
return providers.getTransactionLastResult(transaction);
139+
}
140+
141+
public signAndSendTransaction = async (transaction: Transaction) => {
142+
const selector = await this.selector;
143+
const wallet = await selector.wallet();
144+
145+
const outcome = await wallet.signAndSendTransaction(transaction);
146+
147+
if (!outcome) {
148+
throw new Error(`Transaction wasn't delivered`);
149+
}
150+
151+
return providers.getTransactionLastResult(outcome);
152+
};
153+
154+
public signAndSendTransactions = async (transactions: Array<Transaction>) => {
155+
const selector = await this.selector;
156+
const wallet = await selector.wallet();
157+
158+
const outcomes = await wallet.signAndSendTransactions({
159+
transactions: transactions,
160+
});
161+
162+
if (!outcomes) {
163+
throw new Error(`Transactions weren't delivered`);
164+
}
165+
166+
return outcomes.map((outcome) =>
167+
providers.getTransactionLastResult(outcome)
168+
);
169+
};
170+
171+
public async getBalance(accountId: string): Promise<string> {
172+
const selector = await this.selector;
173+
const { network } = selector.options;
174+
const provider = new providers.JsonRpcProvider({ url: network.nodeUrl });
175+
176+
const request: RpcQueryRequest = {
177+
request_type: "view_account",
178+
account_id: accountId,
179+
finality: "final",
180+
};
181+
const account = await provider.query<AccountView>(request);
182+
183+
return account.amount || "0";
184+
}
185+
186+
public async getFormattedBalance(
187+
accountId: string,
188+
fracDigits?: number
189+
): Promise<string> {
190+
const selector = await this.selector;
191+
const { network } = selector.options;
192+
const provider = new providers.JsonRpcProvider({ url: network.nodeUrl });
193+
194+
const request: RpcQueryRequest = {
195+
request_type: "view_account",
196+
account_id: accountId,
197+
finality: "final",
198+
};
199+
const account = await provider.query<AccountView>(request);
200+
201+
if (!account.amount) {
202+
return "0";
203+
}
204+
205+
return utils.format.formatNearAmount(account.amount, fracDigits);
206+
}
207+
208+
public async viewAccessKey(accountId: string, publicKey: string) {
209+
const selector = await this.selector;
210+
const { network } = selector.options;
211+
const provider = new providers.JsonRpcProvider({ url: network.nodeUrl });
212+
213+
const request: RpcQueryRequest = {
214+
request_type: "view_access_key",
215+
account_id: accountId,
216+
public_key: publicKey,
217+
finality: "final",
218+
};
219+
return await provider.query<AccessKeyView>(request);
220+
}
221+
222+
public async viewAccessKeys(accountId: string) {
223+
const selector = await this.selector;
224+
const { network } = selector.options;
225+
const provider = new providers.JsonRpcProvider({ url: network.nodeUrl });
226+
227+
const request: RpcQueryRequest = {
228+
request_type: "view_access_key_list",
229+
account_id: accountId,
230+
finality: "final",
231+
};
232+
return await provider.query<AccessKeyList>(request);
233+
}
234+
235+
public async viewState(accountId: string, prefix?: string) {
236+
const selector = await this.selector;
237+
const { network } = selector.options;
238+
const provider = new providers.JsonRpcProvider({ url: network.nodeUrl });
239+
240+
const prefixBase64 = prefix ? Buffer.from(prefix).toString("base64") : "";
241+
const request: RpcQueryRequest = {
242+
request_type: "view_state",
243+
account_id: accountId,
244+
prefix_base64: prefixBase64,
245+
finality: "final",
246+
};
247+
return await provider.query<ViewStateResult>(request);
248+
}
249+
250+
public async viewContractCode(accountId: string) {
251+
const selector = await this.selector;
252+
const { network } = selector.options;
253+
const provider = new providers.JsonRpcProvider({ url: network.nodeUrl });
254+
255+
const request: RpcQueryRequest = {
256+
request_type: "view_code",
257+
account_id: accountId,
258+
finality: "final",
259+
};
260+
return await provider.query<CodeResult>(request);
261+
}
262+
}

0 commit comments

Comments
 (0)