@otoplo/wallet-common 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/dist/index.d.ts +2278 -0
  4. package/dist/index.js +2005 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +56 -0
  7. package/src/index.ts +5 -0
  8. package/src/persistence/datastore/db.ts +47 -0
  9. package/src/persistence/datastore/index.ts +2 -0
  10. package/src/persistence/datastore/kv.ts +129 -0
  11. package/src/persistence/index.ts +2 -0
  12. package/src/persistence/wallet-db.ts +251 -0
  13. package/src/services/asset.ts +220 -0
  14. package/src/services/cache.ts +54 -0
  15. package/src/services/discovery.ts +110 -0
  16. package/src/services/index.ts +8 -0
  17. package/src/services/key.ts +55 -0
  18. package/src/services/rostrum.ts +214 -0
  19. package/src/services/session.ts +225 -0
  20. package/src/services/transaction.ts +388 -0
  21. package/src/services/tx-transformer.ts +244 -0
  22. package/src/services/wallet.ts +650 -0
  23. package/src/state/hooks.ts +45 -0
  24. package/src/state/index.ts +3 -0
  25. package/src/state/slices/auth.ts +28 -0
  26. package/src/state/slices/dapp.ts +32 -0
  27. package/src/state/slices/index.ts +6 -0
  28. package/src/state/slices/loader.ts +31 -0
  29. package/src/state/slices/notifications.ts +44 -0
  30. package/src/state/slices/status.ts +70 -0
  31. package/src/state/slices/wallet.ts +112 -0
  32. package/src/state/store.ts +24 -0
  33. package/src/types/dapp.types.ts +21 -0
  34. package/src/types/db.types.ts +142 -0
  35. package/src/types/index.ts +5 -0
  36. package/src/types/notification.types.ts +13 -0
  37. package/src/types/rostrum.types.ts +161 -0
  38. package/src/types/wallet.types.ts +62 -0
  39. package/src/utils/asset.ts +103 -0
  40. package/src/utils/common.ts +159 -0
  41. package/src/utils/enums.ts +22 -0
  42. package/src/utils/index.ts +7 -0
  43. package/src/utils/keypath.ts +15 -0
  44. package/src/utils/price.ts +40 -0
  45. package/src/utils/seed.ts +57 -0
  46. package/src/utils/vault.ts +39 -0
@@ -0,0 +1,62 @@
1
+ import type { DAppInfo, SessionDetails } from "wallet-comms-sdk";
2
+ import type { AssetEntity } from "./db.types";
3
+ import type { AccountType, KeySpace } from "../utils/enums";
4
+
5
+ export interface KeyPath {
6
+ account: AccountType;
7
+ type: KeySpace;
8
+ index: number;
9
+ }
10
+
11
+ export interface AddressKey {
12
+ keyPath: string;
13
+ address: string;
14
+ balance: Balance;
15
+ tokensBalance: Record<string, Balance>;
16
+ }
17
+
18
+ export interface Balance {
19
+ confirmed: string | number;
20
+ unconfirmed: string | number;
21
+ }
22
+
23
+ export interface Price {
24
+ value: number;
25
+ change: number;
26
+ }
27
+
28
+ export interface Account {
29
+ id: number;
30
+ name: string;
31
+ address: string;
32
+ balance: Balance;
33
+ tokensBalance: Record<string, Balance>;
34
+ tokens: AssetEntity[];
35
+ sessions: Record<string, SessionInfo>
36
+ }
37
+
38
+ export interface VaultInfo {
39
+ address: string;
40
+ balance: Balance;
41
+ block: number;
42
+ index: number;
43
+ }
44
+
45
+ export interface NftPreview {
46
+ infoJson: string;
47
+ image: string;
48
+ }
49
+
50
+ export interface SessionInfo {
51
+ details: SessionDetails;
52
+ appInfo: DAppInfo;
53
+ };
54
+
55
+ export interface AssetInfo {
56
+ token: string;
57
+ tokenIdHex: string;
58
+ decimals: number;
59
+ name?: string;
60
+ ticker?: string;
61
+ iconUrl?: string;
62
+ }
@@ -0,0 +1,103 @@
1
+ import { BufferUtils, GroupToken } from "libnexa-ts";
2
+ import type { TokenEntity } from "../types/db.types";
3
+ import { getAddressBuffer, isTestnet } from "./common";
4
+
5
+ export function getNiftyToken(): TokenEntity {
6
+ return {
7
+ name: 'NiftyArt',
8
+ ticker: 'NIFTY',
9
+ iconUrl: 'https://niftyart.cash/td/niftyicon.svg',
10
+ parentGroup: '',
11
+ token: isTestnet()
12
+ ? 'nexatest:tq8r37lcjlqazz7vuvug84q2ev50573hesrnxkv9y6hvhhl5k5qqqnmyf79mx'
13
+ : 'nexa:tr9v70v4s9s6jfwz32ts60zqmmkp50lqv7t0ux620d50xa7dhyqqqcg6kdm6f',
14
+ tokenIdHex: isTestnet()
15
+ ? '0e38fbf897c1d10bcce33883d40acb28fa7a37cc0733598526aecbdff4b50000'
16
+ : 'cacf3d958161a925c28a970d3c40deec1a3fe06796fe1b4a7b68f377cdb90000',
17
+ decimals: 0
18
+ };
19
+ }
20
+
21
+ export function fetchNiftyNFT(id: string): Promise<Uint8Array> {
22
+ return performGet(getNiftyEndpoint() + id, 'raw');
23
+ }
24
+
25
+ export function isNiftySubgroup(group: string): boolean {
26
+ try {
27
+ const addrBuf = getAddressBuffer(group);
28
+ if (!GroupToken.isSubgroup(addrBuf)) {
29
+ return false;
30
+ }
31
+
32
+ return BufferUtils.bufferToHex(addrBuf.subarray(0, 32)) === getNiftyToken().tokenIdHex;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ export function fetchAssetDoc(url: string): Promise<any> {
39
+ // it is possible that legacy token description is stored on IPFS, check for this
40
+ // example: ipfs://bafkvmicdm2vdjgieqvk3s5ykqlddtzd42yuy7voum4cifpknjvhv3uarwu
41
+ url = translateIfIpfsUrl(url);
42
+ return performGet(url, 'json');
43
+ }
44
+
45
+ export function fetchAssetBlob(url: string): Promise<Uint8Array> {
46
+ // it is possible that NRC (Token / NFT) description is stored on IPFS, check for this
47
+ // example: ipfs://bafkvmicdm2vdjgieqvk3s5ykqlddtzd42yuy7voum4cifpknjvhv3uarwu
48
+ url = translateIfIpfsUrl(url);
49
+ return performGet(url, 'raw');
50
+ }
51
+
52
+ export function transformTokenIconUrl(icon: string, documentUrl: string): string {
53
+ if (icon && typeof icon === 'string') {
54
+ if (icon.startsWith('http')) {
55
+ return icon.replace('http://', 'https://');
56
+ }
57
+ if (icon.startsWith('ipfs://')) {
58
+ return icon;
59
+ }
60
+ const url = new URL(documentUrl);
61
+ return `${url.origin}${icon}`;
62
+ }
63
+ return "";
64
+ }
65
+
66
+ export function parseTokenDataUrl(data?: string): string | null {
67
+ if (!data) {
68
+ return null;
69
+ }
70
+ return translateIfIpfsUrl(data);
71
+ }
72
+
73
+ function translateIfIpfsUrl(url: string, gateway = "https://ipfs.nebula.markets/"): string {
74
+ // if ipfs gateway is implemented properly, you will be able to trim
75
+ // the ipfs:// from the url and append the url to the gateway path
76
+ // see https://docs.ipfs.tech/concepts/ipfs-gateway/#path for more info
77
+ if (url?.startsWith("ipfs://")) {
78
+ return gateway + url.substring(7);
79
+ }
80
+ return url;
81
+ }
82
+
83
+ function getNiftyEndpoint(): string {
84
+ return `https://${isTestnet() ? 'testnet.' : ''}niftyart.cash/_public/`;
85
+ }
86
+
87
+ async function performGet(url: string, responseType?: 'json' | 'raw'): Promise<any> {
88
+ try {
89
+ const response = await fetch(url);
90
+ if (!response.ok) {
91
+ throw new Error(`Failed to perform GET. status: ${response.status}`);
92
+ }
93
+
94
+ if (responseType === 'raw') {
95
+ const arrayBuffer = await response.arrayBuffer();
96
+ return new Uint8Array(arrayBuffer);
97
+ } else {
98
+ return await response.json();
99
+ }
100
+ } catch (e) {
101
+ throw new Error(`Unexpected Error: ${e}`);
102
+ }
103
+ }
@@ -0,0 +1,159 @@
1
+ import { Address, AddressType, BufferUtils, CommonUtils, Networks } from "libnexa-ts";
2
+ import type { Balance } from "../types/wallet.types";
3
+
4
+ export const MAIN_WALLET_ID = -1;
5
+ export const MAX_INT64 = 9223372036854775807n;
6
+ export const VAULT_FIRST_BLOCK = 274710;
7
+ export const VAULT_SCRIPT_PREFIX = "0014461ad25081cb0119d034385ff154c8d3ad6bdd76";
8
+
9
+ export function isTestnet(): boolean {
10
+ return Networks.defaultNetwork == Networks.testnet;
11
+ }
12
+
13
+ export function isGenesisHashValid(hash: string): boolean {
14
+ if(isTestnet()) {
15
+ return hash == "508c843a4b98fb25f57cf9ebafb245a5c16468f06519cdd467059a91e7b79d52";
16
+ }
17
+ return hash == "edc7144fe1ba4edd0edf35d7eea90f6cb1dba42314aa85da8207e97c5339c801";
18
+ }
19
+
20
+ export function isValidNexaAddress(address: string, type = AddressType.PayToScriptTemplate): boolean {
21
+ return Address.isValid(address, Networks.defaultNetwork, type);
22
+ }
23
+
24
+ export function getExplorerUrl(): string {
25
+ return `https://${(isTestnet() ? 'testnet-' : '')}explorer.nexa.org`;
26
+ }
27
+
28
+ export function currentTimestamp(): number {
29
+ return Math.floor(Date.now() / 1000);
30
+ }
31
+
32
+ export function estimateDateByFutureBlock(current: number, future: number): string {
33
+ const estimateMins = (future - current) * 2;
34
+ const time = new Date();
35
+ time.setMinutes(time.getMinutes() + estimateMins);
36
+ return time.toLocaleDateString();
37
+ }
38
+
39
+ export function isNullOrEmpty(arg?: string | any[] | null): arg is undefined | [] | null | '' {
40
+ return !arg || arg.length === 0;
41
+ }
42
+
43
+ export function truncateStringMiddle(str?: string, maxLength = 0): string {
44
+ if (!str || str.length <= maxLength) {
45
+ return str || '';
46
+ }
47
+
48
+ const ellipsis = '...';
49
+ const halfLength = Math.floor((maxLength - ellipsis.length) / 2);
50
+ const firstHalf = str.slice(0, halfLength);
51
+ const secondHalf = str.slice(str.length - halfLength);
52
+
53
+ return firstHalf + ellipsis + secondHalf;
54
+ };
55
+
56
+ export function capitalizeFirstLetter(str: string): string {
57
+ if (str.length === 0) {
58
+ return "";
59
+ }
60
+ return str.charAt(0).toUpperCase() + str.substring(1);
61
+ }
62
+
63
+ export function getAddressBuffer(address: string): Uint8Array {
64
+ if (CommonUtils.isHexa(address)) {
65
+ return BufferUtils.hexToBuffer(address) ;
66
+ }
67
+ return Address.fromString(address).data;
68
+ }
69
+
70
+ export function tokenIdToHex(token: string): string {
71
+ if (CommonUtils.isHexa(token)) {
72
+ return token;
73
+ }
74
+ return BufferUtils.bufferToHex(getAddressBuffer(token));
75
+ }
76
+
77
+ export function tokenHexToAddr(token: string): string {
78
+ if (!CommonUtils.isHexa(token)) {
79
+ return token;
80
+ }
81
+ return new Address(getAddressBuffer(token), Networks.defaultNetwork, AddressType.GroupIdAddress).toString();
82
+ }
83
+
84
+ export function tokenAmountToAssetAmount(amount?: string | number | bigint): string {
85
+ if (amount == null || amount === "") {
86
+ return "";
87
+ }
88
+ if (BigInt(amount) <= 0n) {
89
+ return "0";
90
+ }
91
+ return amount.toString();
92
+ }
93
+
94
+ export function sumBalance(balances: Balance[]): Balance {
95
+ let confirmed = BigInt(0), unconfirmed = BigInt(0);
96
+ balances.forEach(b => {
97
+ confirmed += BigInt(b.confirmed);
98
+ unconfirmed += BigInt(b.unconfirmed);
99
+ });
100
+ return {confirmed: confirmed.toString(), unconfirmed: unconfirmed.toString()};
101
+ }
102
+
103
+ export function sumTokensBalance(balances: Record<string, Balance>[]): Record<string, Balance> {
104
+ const tokensBalance: Record<string, Balance> = {};
105
+ balances.forEach(b => {
106
+ for (const key in b) {
107
+ if (tokensBalance[key]) {
108
+ tokensBalance[key].confirmed = (BigInt(tokensBalance[key].confirmed) + BigInt(b[key].confirmed)).toString();
109
+ tokensBalance[key].unconfirmed = (BigInt(tokensBalance[key].unconfirmed) + BigInt(b[key].unconfirmed)).toString();
110
+ } else {
111
+ tokensBalance[key] = { confirmed: BigInt(b[key].confirmed).toString(), unconfirmed: BigInt(b[key].unconfirmed).toString() };
112
+ }
113
+ }
114
+ });
115
+
116
+ return tokensBalance;
117
+ }
118
+
119
+ const FILE_EXTENSION_TO_TYPE = {
120
+ // Images
121
+ //'.svg': { media: 'image', mime: 'image/svg+xml' },
122
+ '.gif': { media: 'image', mime: 'image/gif' },
123
+ '.png': { media: 'image', mime: 'image/png' },
124
+ '.apng': { media: 'image', mime: 'image/apng' },
125
+ '.jpg': { media: 'image', mime: 'image/jpeg' },
126
+ '.jpeg': { media: 'image', mime: 'image/jpeg' },
127
+ '.avif': { media: 'image', mime: 'image/avif' },
128
+ '.webp': { media: 'image', mime: 'image/webp' },
129
+ '.bmp': { media: 'image', mime: 'image/bmp' },
130
+
131
+ // Videos
132
+ '.mp4': { media: 'video', mime: 'video/mp4' },
133
+ '.mpeg': { media: 'video', mime: 'video/mpeg' },
134
+ '.mpg': { media: 'video', mime: 'video/mpeg' },
135
+ '.webm': { media: 'video', mime: 'video/webm' },
136
+ '.mov': { media: 'video', mime: 'video/quicktime' },
137
+
138
+ // Audio
139
+ '.mp3': { media: 'audio', mime: 'audio/mpeg' },
140
+ '.ogg': { media: 'audio', mime: 'audio/ogg' },
141
+ '.wav': { media: 'audio', mime: 'audio/wav' },
142
+ '.m4a': { media: 'audio', mime: 'audio/mp4' },
143
+ } as const;
144
+
145
+ export function getFileMediaType(filename: string): 'video' | 'audio' | 'image' | 'unknown' {
146
+ const lastDotIndex = filename.lastIndexOf('.');
147
+ if (lastDotIndex === -1) return 'unknown';
148
+
149
+ const extension = filename.slice(lastDotIndex).toLowerCase();
150
+ return FILE_EXTENSION_TO_TYPE[extension as keyof typeof FILE_EXTENSION_TO_TYPE]?.media ?? 'unknown';
151
+ }
152
+
153
+ export function getFileMimeType(filename: string): string {
154
+ const lastDotIndex = filename.lastIndexOf('.');
155
+ if (lastDotIndex === -1) return 'application/octet-stream';
156
+
157
+ const extension = filename.slice(lastDotIndex).toLowerCase();
158
+ return FILE_EXTENSION_TO_TYPE[extension as keyof typeof FILE_EXTENSION_TO_TYPE]?.mime ?? 'application/octet-stream';
159
+ }
@@ -0,0 +1,22 @@
1
+ export enum AccountType {
2
+ MAIN = 0,
3
+ VAULT = 1,
4
+ DAPP = 2
5
+ }
6
+
7
+ export enum KeySpace {
8
+ RECEIVE = 0,
9
+ CHANGE = 1
10
+ }
11
+
12
+ export enum SessionRequestType {
13
+ SignMessage = 'signMessage',
14
+ AddToken = 'addToken',
15
+ SignTransaction = 'signTransaction',
16
+ SendTransaction = 'sendTransaction',
17
+ }
18
+
19
+ export enum AssetType {
20
+ TOKEN = 'token',
21
+ NFT = 'nft'
22
+ }
@@ -0,0 +1,7 @@
1
+ export * from './asset';
2
+ export * from './common';
3
+ export * from './enums';
4
+ export * from './seed';
5
+ export * from './price';
6
+ export * from './vault';
7
+ export * from './keypath';
@@ -0,0 +1,15 @@
1
+ import type { KeyPath } from "../types";
2
+ import type { AccountType, KeySpace } from "./enums";
3
+
4
+ export function keyPathToString(account: AccountType, type: KeySpace, index: number): string {
5
+ return `${account}'/${type}/${index}`;
6
+ }
7
+
8
+ export function stringToKeyPath(path: string): KeyPath {
9
+ const parts = path.split('/');
10
+ return {
11
+ account: parseInt(parts[0].replace("'", "")) as AccountType,
12
+ type: parseInt(parts[1]) as KeySpace,
13
+ index: parseInt(parts[2])
14
+ };
15
+ }
@@ -0,0 +1,40 @@
1
+ import type { Price } from "../types/wallet.types";
2
+
3
+ const currencySymbols = {
4
+ usd: "$",
5
+ eur: "€",
6
+ gbp: "£",
7
+ cny: "¥",
8
+ jpy: "¥",
9
+ aud: "A$",
10
+ cad: "C$",
11
+ chf: "Fr"
12
+ } as const;
13
+
14
+ type Currency = keyof typeof currencySymbols;
15
+
16
+ export const currencies = Object.keys(currencySymbols);
17
+
18
+ export async function getNexaPrices(): Promise<Record<string, number>> {
19
+ const vs_currencies = currencies.join(",");
20
+
21
+ const res = await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=nexacoin&include_24hr_change=true&vs_currencies=${vs_currencies}`);
22
+ if (!res.ok) {
23
+ throw new Error("Failed to fetch price");
24
+ }
25
+
26
+ const data = await res.json();
27
+ return data.nexacoin || {};
28
+ }
29
+
30
+ export function getCurrencySymbol(currency: Currency): string {
31
+ return currencySymbols[currency] || currency;
32
+ }
33
+
34
+ export function initializePrices(): Record<string, Price> {
35
+ const prices: Record<string, Price> = {};
36
+ currencies.forEach(currency => {
37
+ prices[currency] = { value: 0, change: 0 };
38
+ });
39
+ return prices;
40
+ }
@@ -0,0 +1,57 @@
1
+ import { gcm } from "@noble/ciphers/aes.js";
2
+ import { pbkdf2Async } from "@noble/hashes/pbkdf2.js";
3
+ import { sha256 } from "@noble/hashes/sha2.js";
4
+ import { generateMnemonic, validateMnemonic } from "@scure/bip39";
5
+ import { wordlist } from "@scure/bip39/wordlists/english.js";
6
+ import { BufferUtils } from "libnexa-ts";
7
+
8
+ export function generateNewMnemonic(size: 12 | 24 = 12): string {
9
+ return generateMnemonic(wordlist, size === 24 ? 256 : 128);
10
+ }
11
+
12
+ export function isMnemonicValid(mnemonic: string): boolean {
13
+ return validateMnemonic(mnemonic, wordlist);
14
+ }
15
+
16
+ async function deriveKey(password: string, salt: Uint8Array): Promise<Uint8Array> {
17
+ return pbkdf2Async(sha256, password, salt, { c: 600_000, dkLen: 32 });
18
+ }
19
+
20
+ export async function encryptMnemonic(phrase: string, password: string): Promise<string> {
21
+ const salt = BufferUtils.getRandomBuffer(16);
22
+ const iv = BufferUtils.getRandomBuffer(12);
23
+ const data = BufferUtils.utf8ToBuffer(phrase);
24
+
25
+ const key = await deriveKey(password, salt);
26
+
27
+ const cipher = gcm(key, iv).encrypt(data);
28
+ const encBuf = BufferUtils.concat([salt, iv, cipher]);
29
+ return BufferUtils.bufferToBase64(encBuf);
30
+ }
31
+
32
+ async function decryptMnemonic(encSeed: string, password: string): Promise<string> {
33
+ const encBuf = BufferUtils.base64ToBuffer(encSeed);
34
+
35
+ const salt = encBuf.subarray(0, 16);
36
+ const iv = encBuf.subarray(16, 28);
37
+ const cipher = encBuf.subarray(28);
38
+
39
+ const key = await deriveKey(password, salt);
40
+
41
+ const data = gcm(key, iv).decrypt(cipher);
42
+ return BufferUtils.bufferToUtf8(data);
43
+ }
44
+
45
+ export async function validateAndDecryptMnemonic(encSeed: string, password: string): Promise<string | boolean> {
46
+ try {
47
+ if (encSeed) {
48
+ const decMn = await decryptMnemonic(encSeed, password);
49
+ if (decMn && isMnemonicValid(decMn)) {
50
+ return decMn;
51
+ }
52
+ }
53
+ return false;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
@@ -0,0 +1,39 @@
1
+ import type { PublicKey, ScriptElement} from "libnexa-ts";
2
+ import { Address, BNExtended, Hash, Opcode, Script, ScriptOpcode } from "libnexa-ts";
3
+
4
+ export function getVaultTemplate(): Script {
5
+ return Script.empty()
6
+ .add(Opcode.OP_FROMALTSTACK).add(Opcode.OP_DROP)
7
+ .add(Opcode.OP_FROMALTSTACK).add(Opcode.OP_CHECKLOCKTIMEVERIFY).add(Opcode.OP_DROP)
8
+ .add(Opcode.OP_FROMALTSTACK).add(Opcode.OP_CHECKSIGVERIFY);
9
+ }
10
+
11
+ export function getVaultTemplateHash(): Uint8Array {
12
+ const template = getVaultTemplate();
13
+ return Hash.sha256ripemd160(template.toBuffer());
14
+ }
15
+
16
+ export function generateVaultConstraint(pubKey: PublicKey): Script {
17
+ return Script.empty().add(pubKey.toBuffer());
18
+ }
19
+
20
+ export function getVaultConstraintHash(pubKey: PublicKey): Uint8Array {
21
+ const constraint = generateVaultConstraint(pubKey);
22
+ return Hash.sha256ripemd160(constraint.toBuffer());
23
+ }
24
+
25
+ export function generateVaultVisibleArgs(args: number[]): ScriptElement[] {
26
+ return args.map(arg => arg <= 16 ? ScriptOpcode.smallInt(arg) : BNExtended.fromNumber(arg).toScriptNumBuffer());
27
+ }
28
+
29
+ export function generateVaultAddress(pubKey: PublicKey, args: number[]): string | undefined {
30
+ if (args.length !== 2) {
31
+ return undefined;
32
+ }
33
+
34
+ const templateHash = getVaultTemplateHash();
35
+ const constraintHash = getVaultConstraintHash(pubKey);
36
+ const visibleArgs = generateVaultVisibleArgs(args);
37
+ const address = Address.fromScriptTemplate(templateHash, constraintHash, visibleArgs);
38
+ return address.toString();
39
+ }