├── .gitignore
├── .prettierrc
├── README.md
├── example
├── index.html
├── index.ts
└── success.html
├── package-lock.json
├── package.json
├── src
├── helpers
│ ├── actions.ts
│ ├── nep0314.ts
│ ├── proxyMethods.ts
│ ├── types.ts
│ ├── utils.ts
│ └── waitInjected.ts
├── index.ts
├── qrcode-strategy
│ ├── core
│ │ ├── index.js
│ │ └── utils.js
│ ├── index.ts
│ ├── logo.ts
│ └── qrcode.ts
├── storage
│ ├── HereKeyStore.ts
│ └── JSONStorage.ts
├── strategies
│ ├── HereStrategy.ts
│ ├── InjectedStrategy.ts
│ ├── TelegramAppStrategy.ts
│ ├── WidgetStrategy.ts
│ └── WindowStrategy.ts
├── telegramEthereumProvider.ts
├── types.ts
└── wallet.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist/example*
3 | dist/index.html
4 | build
5 | dist
6 | .cache
7 | .DS_Store
8 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 150
3 | }
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @herewallet/core
2 |
3 | In contrast to the synchronous signing of transactions in web near wallets, where the user is redirected to the wallet site for signing -- **HERE Wallet** provides the ability to sign transactions using async/await API calls.
4 |
5 | ```bash
6 | npm i near-api-js@^0.44.2 --save
7 | npm i @here-wallet/core --save
8 | ```
9 |
10 | ## Usage
11 |
12 | ```ts
13 | import { HereWallet } from "@here-wallet/core";
14 | const here = await HereWallet.connect();
15 | const account = await here.signIn({ contractId: "social.near" });
16 | console.log(`Hello ${account}!`);
17 | ```
18 |
19 | **You can also login to the wallet without adding a key. For this you can call `signIn` without `contractId`**
20 |
21 | ## How it works
22 |
23 | By default, all near-selector api calls that you make with this library run a background process and generate a unique link that the user can go to their mobile wallet and confirm the transaction. This is a link of the form: https://h4n.app/TRX_PART_OF_SHA1_IN_BASE64
24 |
25 | If a user has logged into your application from a phone and has a wallet installed, we immediately transfer him to the application for signing. In all other cases, we open a new window on the web.herewallet.app site, where the user can find information about installing the wallet and sign the transaction there.
26 |
27 | All this time while user signing the transaction, a background process in your application will monitor the status of the transaction requested for signing.
28 |
29 | ## Sign in is optional!
30 |
31 | You can generate a signing transaction without knowing your user's accountId (without calling signIn).
32 | There are cases when you do not need to receive a public key from the user to call your contract, but you want to ask the user to perform an action in your application once:
33 |
34 | ```ts
35 | import { HereWallet } from "@here-wallet/core";
36 | const here = await HereWallet.connect();
37 | const tx = await here.signAndSendTransaction({
38 | actions: [{ type: "FunctionCall", params: { deposit: 1000 } }],
39 | receiverId: "donate.near",
40 | });
41 |
42 | console.log("Thanks for the donation!");
43 | ```
44 |
45 | ## Build Telegram App and connect HOT Telegram Wallet
46 |
47 | ```ts
48 | import { HereWallet } from "@here-wallet/core";
49 | const here = await HereWallet.connect({
50 | botId: "HOTExampleConnectBot/app", // Your bot MiniApp
51 | walletId: "herewalletbot/app", // HOT Wallet
52 | });
53 | ```
54 |
55 | ## Login without AddKey
56 |
57 | In order to use the wallet for authorization on the backend, you need to use the signMessage method.
58 | This method signs your message with a private full access key inside the wallet. You can also use this just to securely get your user's accountId without any extra transactions.
59 |
60 | ```ts
61 | import { HereWallet } from "@here-wallet/core";
62 | const here = await HereWallet.connect();
63 |
64 | const nonce = Array.from(crypto.getRandomValues(new Uint8Array(32)));
65 | const recipient = window.location.host;
66 | const message = "Authenticate message";
67 |
68 | const { signature, publicKey, accountId } = await here.signMessage({ recipient, nonce, message });
69 |
70 | // Verify on you backend side, check NEP0413
71 | const accessToken = await axios.post(yourAPI, { signature, accountId, publicKey, nonce, message, recipient });
72 | console.log("Auth completed!");
73 | ```
74 |
75 | Or you can verify signMessage on client side, just call:
76 |
77 | ```ts
78 | try {
79 | const { accountId } = await here.authenticate();
80 | console.log(`Hello ${accountId}!`);
81 | } catch (e) {
82 | console.log(e);
83 | }
84 | ```
85 |
86 | If you use js-sdk on your backend, then you do not need to additionally check the signature and key, the library does this, and if the signature is invalid or the key is not a full access key, then the method returns an error.
87 | Otherwise, on the backend, you need to verify the signature and message with this public key. And also check that this public key is the full access key for this accountId.
88 |
89 | **It's important to understand** that the returned message is not the same as the message you submitted for signature.
90 | This message conforms to the standard: https://github.com/near/NEPs/pull/413
91 |
92 | ## Instant Wallet with AppClip
93 |
94 | If your goal is to provide the user with a convenient way to log in to your desktop app, you can use Here Instant Wallet, which allows users without a wallet to instantly create one via appclip.
95 |
96 | > At the moment here wallet is only available for IOS users
97 |
98 | You have the option to override how your user is delivered the signing link.
99 | This is how you can create a long-lived transaction signature request and render it on your web page:
100 |
101 | ```ts
102 | import { HereStrategy, HereWallet } from "@here-wallet/core";
103 | import { QRCodeStrategy } from "@here-wallet/core/qrcode-strategy";
104 |
105 | const putQrcode = document.getElementById("qr-container");
106 |
107 | // Instant wallet signin HERE!
108 | const here = await HereWallet.connect();
109 | await here.signIn({
110 | contractId: "social.near",
111 | // override default connect strategy
112 | strategy: new QRCodeStrategy({
113 | element: putQrcode,
114 | theme: "dark",
115 | size: 128,
116 | }),
117 | });
118 | ```
119 |
120 | You can also look at an example in this repository /example/index.ts or in sandbox:
121 | https://codesandbox.io/s/here-wallet-instant-app-6msgmn
122 |
123 | ## Security
124 |
125 | To transfer data between the application and the phone, we use our own proxy service.
126 | On the client side, a transaction confirmation request is generated with a unique request_id, our wallet receives this request_id and requests this transaction from the proxy.
127 |
128 | **To make sure that the transaction was not forged by the proxy service, the link that opens inside the application contains a hash-sum of the transaction. If the hashes do not match, the wallet will automatically reject the signing request**
129 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | HERE Wallet
5 |
6 |
7 |
8 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | Sign message
76 | Function call
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/example/index.ts:
--------------------------------------------------------------------------------
1 | import { QRCodeStrategy } from "../src/qrcode-strategy";
2 | import { HereWallet } from "../src";
3 |
4 | const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
5 |
6 | const uikit = {
7 | connectBtn: document.getElementById("connect")!,
8 | qrcode: document.getElementById("qrcode")!,
9 | account: document.getElementById("account-id")!,
10 | fnCallBtn: document.getElementById("function-call")!,
11 | verifyBtn: document.getElementById("verify-connect")!,
12 |
13 | loginState(name) {
14 | uikit.account.innerHTML = `Hello ${name}!`;
15 | uikit.connectBtn.innerHTML = "Logout";
16 | uikit.fnCallBtn.style.display = "block";
17 | uikit.qrcode.style.display = "none";
18 | uikit.qrcode.innerHTML = "";
19 | },
20 |
21 | logoutState() {
22 | uikit.account.innerHTML = `Scan QR to sigin with AppClip!`;
23 | uikit.connectBtn.innerHTML = "Connect HERE Wallet";
24 | uikit.fnCallBtn.style.display = "none";
25 | uikit.qrcode.style.display = "block";
26 | },
27 | };
28 |
29 | // Instant wallet signin HERE!
30 | const instantSignin = async (here) => {
31 | class MyQrCodeStrategy extends QRCodeStrategy {
32 | async onApproving(r) {
33 | console.log("onApproving", r);
34 | }
35 |
36 | async onSuccess(r) {
37 | await super.onSuccess(r);
38 | console.log("onSuccess", r);
39 | }
40 |
41 | async onFailed(r) {
42 | await super.onFailed(r);
43 | console.log("onFailed");
44 | await delay(3000);
45 | await instantSignin(this.wallet);
46 | }
47 | }
48 |
49 | const account = await here.signIn({
50 | strategy: new MyQrCodeStrategy({ element: uikit.qrcode }),
51 | contractId: "social.near",
52 | });
53 |
54 | uikit.loginState(account);
55 | };
56 |
57 | const main = async () => {
58 | const here = await HereWallet.connect({});
59 |
60 | if (await here.isSignedIn()) {
61 | uikit.loginState(await here.getAccountId());
62 | } else {
63 | uikit.logoutState();
64 | instantSignin(here);
65 | }
66 |
67 | uikit.verifyBtn.onclick = async () => {
68 | const signed = await here.authenticate();
69 | alert("Signed by " + signed.accountId);
70 | };
71 |
72 | uikit.connectBtn.onclick = async () => {
73 | if (await here.isSignedIn()) {
74 | here.signOut();
75 | uikit.logoutState();
76 | await instantSignin(here);
77 | return;
78 | }
79 |
80 | const account = await here.signIn({ contractId: "social.near" });
81 | uikit.loginState(account);
82 | };
83 |
84 | uikit.fnCallBtn?.addEventListener("click", async () => {
85 | const account = await here.getAccountId();
86 | const result = await here.signAndSendTransactions({
87 | callbackUrl: "/success",
88 | transactions: [
89 | {
90 | receiverId: "social.near",
91 | actions: [
92 | {
93 | type: "FunctionCall",
94 | params: {
95 | methodName: "set",
96 | args: { data: { [account]: { profile: { hereUser: "yes" } } } },
97 | gas: "30000000000000",
98 | deposit: "1",
99 | },
100 | },
101 | ],
102 | },
103 | ],
104 | });
105 |
106 | console.log(result);
107 | });
108 | };
109 |
110 | main();
111 |
--------------------------------------------------------------------------------
/example/success.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | HERE Wallet
5 |
6 |
7 |
8 |
69 |
70 |
71 |
72 | Transaction success
73 | Open explorer
74 | Back
75 |
76 |
77 |
78 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@here-wallet/core",
3 | "version": "3.4.0",
4 | "description": "HereWallet lib for near-selector",
5 | "main": "build/index.js",
6 | "types": "build/index.d.ts",
7 | "type": "commonjs",
8 | "exports": {
9 | ".": "./build/index.js",
10 | "./qrcode-strategy": "./build/qrcode-strategy/index.js",
11 | "./telegramEthereumProvider": "./build/telegramEthereumProvider.js"
12 | },
13 | "typesVersions": {
14 | "*": {
15 | "telegramEthereumProvider": [
16 | "build/telegramEthereumProvider.d.ts"
17 | ],
18 | "qrcode-strategy": [
19 | "build/qrcode-strategy/index.d.ts"
20 | ]
21 | }
22 | },
23 | "homepage": "https://github.com/here-wallet/near-selector",
24 | "repository": {
25 | "type": "git",
26 | "url": "git+ssh://git@github.com/here-wallet/near-selector.git"
27 | },
28 | "scripts": {
29 | "example": "parcel example/index.html --open --port 3000",
30 | "build:example": "parcel build example/index.html --port 3000",
31 | "build": "tsc"
32 | },
33 | "dependencies": {
34 | "@near-js/accounts": "^1.2.1",
35 | "@near-js/crypto": "^1.2.4",
36 | "@near-js/types": "^0.2.1",
37 | "@near-js/utils": "^0.2.2",
38 | "js-sha256": "^0.11.0",
39 | "sha1": "^1.1.1",
40 | "uuid4": "2.0.3"
41 | },
42 | "peerDependencies": {
43 | "bn.js": "5.2.1",
44 | "borsh": "0.7.0"
45 | },
46 | "devDependencies": {
47 | "@babel/core": "7.2.0",
48 | "@types/bn.js": "5.1.1",
49 | "@types/sha1": "^1.1.3",
50 | "@types/uuid4": "^2.0.0",
51 | "parcel-bundler": "^1.6.1",
52 | "typescript": "4.4.4"
53 | },
54 | "keywords": [
55 | "near",
56 | "hot-coin",
57 | "ethereum",
58 | "herewallet",
59 | "web3"
60 | ],
61 | "bugs": {
62 | "url": "https://github.com/here-wallet/near-selector/issues"
63 | },
64 | "author": "here-wallet",
65 | "license": "ISC",
66 | "directories": {
67 | "example": "example"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/helpers/actions.ts:
--------------------------------------------------------------------------------
1 | import { PublicKey } from "@near-js/crypto";
2 | import { actionCreators } from "@near-js/transactions";
3 | import { Action, AddKeyPermission } from "./types";
4 |
5 | const getAccessKey = (permission: AddKeyPermission) => {
6 | if (permission === "FullAccess") {
7 | return actionCreators.fullAccessKey();
8 | }
9 |
10 | const { receiverId, methodNames = [] } = permission;
11 | const allowance = permission.allowance ? BigInt(permission.allowance) : undefined;
12 | return actionCreators.functionCallAccessKey(receiverId, methodNames, allowance);
13 | };
14 |
15 | export const parseArgs = (data: Object | string) => {
16 | if (typeof data === "string") return Buffer.from(data, "base64");
17 | return data;
18 | };
19 |
20 | export const createAction = (action: Action) => {
21 | switch (action.type) {
22 | case "CreateAccount":
23 | return actionCreators.createAccount();
24 |
25 | case "DeployContract": {
26 | const { code } = action.params;
27 | return actionCreators.deployContract(code);
28 | }
29 |
30 | case "FunctionCall": {
31 | const { methodName, args, gas, deposit } = action.params;
32 | return actionCreators.functionCall(methodName, parseArgs(args), BigInt(gas), BigInt(deposit));
33 | }
34 |
35 | case "Transfer": {
36 | const { deposit } = action.params;
37 | return actionCreators.transfer(BigInt(deposit));
38 | }
39 |
40 | case "Stake": {
41 | const { stake, publicKey } = action.params;
42 | return actionCreators.stake(BigInt(stake), PublicKey.from(publicKey));
43 | }
44 |
45 | case "AddKey": {
46 | const { publicKey, accessKey } = action.params;
47 | return actionCreators.addKey(
48 | PublicKey.from(publicKey), // TODO: Use accessKey.nonce? near-api-js seems to think 0 is fine?
49 | getAccessKey(accessKey.permission)
50 | );
51 | }
52 |
53 | case "DeleteKey": {
54 | const { publicKey } = action.params;
55 | return actionCreators.deleteKey(PublicKey.from(publicKey));
56 | }
57 |
58 | case "DeleteAccount": {
59 | const { beneficiaryId } = action.params;
60 | return actionCreators.deleteAccount(beneficiaryId);
61 | }
62 |
63 | default:
64 | throw new Error("Invalid action type");
65 | }
66 | };
67 |
--------------------------------------------------------------------------------
/src/helpers/nep0314.ts:
--------------------------------------------------------------------------------
1 | import * as borsh from "borsh";
2 | import js_sha256 from "js-sha256";
3 | import { PublicKey } from "@near-js/crypto";
4 | import { SignedMessageNEP0413, SignMessageOptionsNEP0413 } from "../types";
5 |
6 | export class AuthPayload implements SignMessageOptionsNEP0413 {
7 | readonly message: string;
8 | readonly recipient: string;
9 | readonly nonce: Buffer;
10 | readonly callbackUrl?: string | undefined;
11 | readonly tag: number;
12 |
13 | constructor({ message, nonce, recipient, callbackUrl }: SignMessageOptionsNEP0413) {
14 | this.tag = 2147484061;
15 | this.message = message;
16 | this.nonce = nonce;
17 | this.recipient = recipient;
18 | if (callbackUrl) {
19 | this.callbackUrl = callbackUrl;
20 | }
21 | }
22 | }
23 |
24 | export const authPayloadSchema: borsh.Schema = {
25 | struct: {
26 | tag: "u32",
27 | message: "string",
28 | nonce: { array: { type: "u8", len: 32 } },
29 | recipient: "string",
30 | callbackUrl: { option: "string" },
31 | },
32 | };
33 |
34 | export function verifySignature(request: SignMessageOptionsNEP0413, result: SignedMessageNEP0413) {
35 | // Reconstruct the payload that was **actually signed**
36 | const payload = new AuthPayload(request);
37 | const borsh_payload = borsh.serialize(authPayloadSchema, payload);
38 | const to_sign = Uint8Array.from(js_sha256.sha256.array(borsh_payload));
39 |
40 | // Reconstruct the signature from the parameter given in the URL
41 | let real_signature = new Uint8Array(Buffer.from(result.signature, "base64"));
42 |
43 | // Use the public Key to verify that the private-counterpart signed the message
44 | const myPK = PublicKey.from(result.publicKey);
45 | return myPK.verify(to_sign, real_signature);
46 | }
47 |
--------------------------------------------------------------------------------
/src/helpers/proxyMethods.ts:
--------------------------------------------------------------------------------
1 | import sha1 from "sha1";
2 | import uuid4 from "uuid4";
3 | import { baseDecode, baseEncode } from "@near-js/utils";
4 |
5 | import { HereProviderRequest, HereProviderResult } from "../types";
6 | import { getDeviceId } from "./utils";
7 |
8 | export const proxyApi = "https://h4n.app";
9 |
10 | export const getRequest = async (id: string, signal?: AbortSignal): Promise => {
11 | const res = await fetch(`${proxyApi}/${id}/request`, {
12 | signal,
13 | headers: { "content-type": "application/json" },
14 | method: "GET",
15 | });
16 |
17 | if (res.ok === false) {
18 | throw Error(await res.text());
19 | }
20 |
21 | const { data } = await res.json();
22 | return JSON.parse(Buffer.from(baseDecode(data)).toString("utf8"));
23 | };
24 |
25 | export const getResponse = async (id: string): Promise => {
26 | const res = await fetch(`${proxyApi}/${id}/response`, {
27 | headers: { "content-type": "application/json" },
28 | method: "GET",
29 | });
30 |
31 | if (res.ok === false) {
32 | throw Error(await res.text());
33 | }
34 |
35 | const { data } = await res.json();
36 | const result: HereProviderResult = JSON.parse(data) ?? {};
37 | return Object.assign({ type: "here", public_key: "", account_id: "", payload: "", status: -1, path: "" }, result);
38 | };
39 |
40 | export const deleteRequest = async (id: string) => {
41 | const res = await fetch(`${proxyApi}/${id}`, {
42 | headers: { "content-type": "application/json" },
43 | method: "DELETE",
44 | });
45 |
46 | if (res.ok === false) {
47 | throw Error(await res.text());
48 | }
49 | };
50 |
51 | export const computeRequestId = async (request: HereProviderRequest) => {
52 | const query = baseEncode(JSON.stringify({ ...request, _id: uuid4() }));
53 | const hashsum = sha1(query);
54 | const id = Buffer.from(hashsum, "hex").toString("base64");
55 | const requestId = id.replaceAll("/", "_").replaceAll("-", "+").slice(0, 13);
56 | return { requestId, query };
57 | };
58 |
59 | export const createRequest = async (request: HereProviderRequest, signal?: AbortSignal) => {
60 | const { query, requestId } = await computeRequestId(request);
61 | const res = await fetch(`${proxyApi}/${requestId}/request`, {
62 | method: "POST",
63 | body: JSON.stringify({ topic_id: getDeviceId(), data: query }),
64 | headers: { "content-type": "application/json" },
65 | signal,
66 | });
67 |
68 | if (res.ok === false) {
69 | throw Error(await res.text());
70 | }
71 |
72 | return requestId;
73 | };
74 |
--------------------------------------------------------------------------------
/src/helpers/types.ts:
--------------------------------------------------------------------------------
1 | export type Base64 = string;
2 | export interface CreateAccountAction {
3 | type: "CreateAccount";
4 | }
5 | export interface DeployContractAction {
6 | type: "DeployContract";
7 | params: {
8 | code: Uint8Array;
9 | };
10 | }
11 | export interface FunctionCallAction {
12 | type: "FunctionCall";
13 | params: {
14 | methodName: string;
15 | args: object | Base64;
16 | gas: string | number;
17 | deposit: string;
18 | };
19 | }
20 | export interface TransferAction {
21 | type: "Transfer";
22 | params: {
23 | deposit: string;
24 | };
25 | }
26 | export interface StakeAction {
27 | type: "Stake";
28 | params: {
29 | stake: string;
30 | publicKey: string;
31 | };
32 | }
33 | export declare type AddKeyPermission =
34 | | "FullAccess"
35 | | {
36 | receiverId: string;
37 | allowance?: string;
38 | methodNames?: Array;
39 | };
40 |
41 | export interface AddKeyAction {
42 | type: "AddKey";
43 | params: {
44 | publicKey: string;
45 | accessKey: {
46 | nonce?: number;
47 | permission: AddKeyPermission;
48 | };
49 | };
50 | }
51 | export interface DeleteKeyAction {
52 | type: "DeleteKey";
53 | params: {
54 | publicKey: string;
55 | };
56 | }
57 | export interface DeleteAccountAction {
58 | type: "DeleteAccount";
59 | params: {
60 | beneficiaryId: string;
61 | };
62 | }
63 |
64 | export declare type Action =
65 | | CreateAccountAction
66 | | DeployContractAction
67 | | FunctionCallAction
68 | | TransferAction
69 | | StakeAction
70 | | AddKeyAction
71 | | DeleteKeyAction
72 | | DeleteAccountAction;
73 |
74 | export declare type ActionType = Action["type"];
75 |
76 | export type Optional = Omit & Partial>;
77 |
78 | export interface Transaction {
79 | signerId: string;
80 | receiverId?: string;
81 | actions: Array;
82 | }
83 |
--------------------------------------------------------------------------------
/src/helpers/utils.ts:
--------------------------------------------------------------------------------
1 | import uuid4 from "uuid4";
2 | import { AccessKeyInfoView } from "@near-js/types";
3 | import { HereProviderError, HereCall, HereProviderResult, HereProviderStatus, SelectorType } from "../types";
4 | import { HereStrategy } from "../strategies/HereStrategy";
5 | import { Action } from "./types";
6 |
7 | export const getDeviceId = () => {
8 | const topicId = window?.localStorage.getItem("herewallet-topic") || uuid4();
9 | window?.localStorage.setItem("herewallet-topic", topicId);
10 | return topicId;
11 | };
12 |
13 | export const isMobile = () => {
14 | return window?.matchMedia("(any-pointer:coarse)").matches || false;
15 | };
16 |
17 | export const serializeActions = (actions: Action[]) => {
18 | return actions.map((act) => {
19 | if (act.type !== "FunctionCall") return act;
20 | let { args, deposit, gas, methodName } = act.params;
21 |
22 | if (ArrayBuffer.isView(args)) {
23 | args = Buffer.from(args.buffer, args.byteOffset, args.byteLength);
24 | }
25 |
26 | if (args instanceof Buffer) {
27 | args = args.toString("base64");
28 | }
29 |
30 | return {
31 | type: act.type,
32 | params: { args, deposit, gas, methodName },
33 | };
34 | });
35 | };
36 |
37 | export const getPublicKeys = async (rpc: string, accountId: string): Promise> => {
38 | const res = await fetch(rpc, {
39 | method: "POST",
40 | body: JSON.stringify({
41 | jsonrpc: "2.0",
42 | id: "dontcare",
43 | method: "query",
44 | params: {
45 | request_type: "view_access_key_list",
46 | finality: "final",
47 | account_id: accountId,
48 | },
49 | }),
50 | headers: {
51 | "content-type": "application/json",
52 | },
53 | });
54 |
55 | if (res.ok === false) {
56 | return [];
57 | }
58 |
59 | const data = await res.json();
60 | return data.result.keys;
61 | };
62 |
63 | export const internalThrow = (error: unknown, strategy: HereStrategy, selector?: SelectorType) => {
64 | if (error instanceof HereProviderError) {
65 | throw error;
66 | }
67 |
68 | const result: HereProviderResult = {
69 | payload: error instanceof Error ? error.message : "UNKNOWN",
70 | status: HereProviderStatus.FAILED,
71 | type: selector?.type || "web",
72 | account_id: selector?.id || "",
73 | };
74 |
75 | strategy.onFailed(result);
76 | throw error;
77 | };
78 |
79 | export const isValidAccessKey = (accountId: string, accessKey: AccessKeyInfoView, call: HereCall) => {
80 | const { permission } = accessKey.access_key;
81 | if (permission === "FullAccess") {
82 | return true;
83 | }
84 |
85 | if (permission.FunctionCall) {
86 | const { receiver_id: allowedReceiverId, method_names: allowedMethods } = permission.FunctionCall;
87 |
88 | /********************************
89 | Accept multisig access keys and let wallets attempt to signAndSendTransaction
90 | If an access key has itself as receiverId and method permission add_request_and_confirm, then it is being used in a wallet with multisig contract: https://github.com/near/core-contracts/blob/671c05f09abecabe7a7e58efe942550a35fc3292/multisig/src/lib.rs#L149-L153
91 | ********************************/
92 | if (allowedReceiverId === accountId && allowedMethods.includes("add_request_and_confirm")) {
93 | return true;
94 | }
95 |
96 | if (allowedReceiverId === call.receiverId) {
97 | if (call.actions.length !== 1) return false;
98 |
99 | return call.actions.every((action) => {
100 | if (action.type !== "FunctionCall") return false;
101 | return (
102 | (!action.params.deposit || action.params.deposit.toString() === "0") &&
103 | (allowedMethods.length === 0 || allowedMethods.includes(action.params.methodName))
104 | );
105 | });
106 | }
107 | }
108 |
109 | return false;
110 | };
111 |
--------------------------------------------------------------------------------
/src/helpers/waitInjected.ts:
--------------------------------------------------------------------------------
1 | export type InjectedState = {
2 | ethAddress?: string;
3 | accountId: string;
4 | network: string;
5 | publicKey: string;
6 | telegramId: number;
7 | };
8 |
9 | export const waitInjectedHereWallet = new Promise((resolve) => {
10 | if (typeof window === "undefined") return resolve(null);
11 | if (window?.self === window?.top) return resolve(null);
12 |
13 | const handler = (e: any) => {
14 | if (e.data.type !== "here-wallet-injected") return;
15 | window?.parent.postMessage("here-sdk-init", "*");
16 | window?.removeEventListener("message", handler);
17 | resolve({
18 | ethAddress: e.data.ethAddress,
19 | accountId: e.data.accountId,
20 | publicKey: e.data.publicKey,
21 | telegramId: e.data.telegramId,
22 | network: e.data.network || "mainnet",
23 | });
24 | };
25 |
26 | window?.addEventListener("message", handler);
27 | setTimeout(() => resolve(null), 2000);
28 | });
29 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { HereWallet } from "./wallet";
2 |
3 | export * from "./helpers/waitInjected";
4 |
5 | export * from "./helpers/proxyMethods";
6 | export * from "./helpers/nep0314";
7 | export * from "./helpers/actions";
8 | export * from "./helpers/types";
9 | export * from "./helpers/utils";
10 |
11 | export * from "./storage/HereKeyStore";
12 | export * from "./storage/JSONStorage";
13 |
14 | export * from "./strategies/HereStrategy";
15 | export * from "./strategies/InjectedStrategy";
16 | export * from "./strategies/TelegramAppStrategy";
17 | export * from "./strategies/WidgetStrategy";
18 | export * from "./strategies/WindowStrategy";
19 | export * from "./types";
20 |
--------------------------------------------------------------------------------
/src/qrcode-strategy/core/index.js:
--------------------------------------------------------------------------------
1 | export const qrcode = function (typeNumber, errorCorrectLevel) {
2 | var PAD0 = 0xec,
3 | PAD1 = 0x11,
4 | _typeNumber = typeNumber,
5 | _errorCorrectLevel = QRErrorCorrectLevel[errorCorrectLevel],
6 | _modules = null,
7 | _moduleCount = 0,
8 | _dataCache = null,
9 | _dataList = new Array(),
10 | _this = {},
11 | makeImpl = function (test, maskPattern) {
12 | _moduleCount = _typeNumber * 4 + 17;
13 | _modules = (function (moduleCount) {
14 | var modules = new Array(moduleCount);
15 | for (var row = 0; row < moduleCount; row += 1) {
16 | modules[row] = new Array(moduleCount);
17 | for (var col = 0; col < moduleCount; col += 1) {
18 | modules[row][col] = null;
19 | }
20 | }
21 | return modules;
22 | })(_moduleCount);
23 |
24 | setupPositionProbePattern(0, 0);
25 | setupPositionProbePattern(_moduleCount - 7, 0);
26 | setupPositionProbePattern(0, _moduleCount - 7);
27 | setupPositionAdjustPattern();
28 | setupTimingPattern();
29 | setupTypeInfo(test, maskPattern);
30 |
31 | if (_typeNumber >= 7) {
32 | setupTypeNumber(test);
33 | }
34 |
35 | if (_dataCache == null) {
36 | _dataCache = createData(_typeNumber, _errorCorrectLevel, _dataList);
37 | }
38 |
39 | mapData(_dataCache, maskPattern);
40 | },
41 | setupPositionProbePattern = function (row, col) {
42 | for (var r = -1; r <= 7; r += 1) {
43 | if (row + r <= -1 || _moduleCount <= row + r) continue;
44 |
45 | for (var c = -1; c <= 7; c += 1) {
46 | if (col + c <= -1 || _moduleCount <= col + c) continue;
47 |
48 | if (
49 | (0 <= r && r <= 6 && (c == 0 || c == 6)) ||
50 | (0 <= c && c <= 6 && (r == 0 || r == 6)) ||
51 | (2 <= r && r <= 4 && 2 <= c && c <= 4)
52 | ) {
53 | _modules[row + r][col + c] = true;
54 | } else {
55 | _modules[row + r][col + c] = false;
56 | }
57 | }
58 | }
59 | },
60 | getBestMaskPattern = function () {
61 | var minLostPoint = 0,
62 | pattern = 0;
63 |
64 | for (var i = 0; i < 8; i += 1) {
65 | makeImpl(true, i);
66 |
67 | var lostPoint = QRUtil.getLostPoint(_this);
68 |
69 | if (i == 0 || minLostPoint > lostPoint) {
70 | minLostPoint = lostPoint;
71 | pattern = i;
72 | }
73 | }
74 |
75 | return pattern;
76 | },
77 | setupTimingPattern = function () {
78 | for (var r = 8; r < _moduleCount - 8; r += 1) {
79 | if (_modules[r][6] != null) {
80 | continue;
81 | }
82 | _modules[r][6] = r % 2 == 0;
83 | }
84 |
85 | for (var c = 8; c < _moduleCount - 8; c += 1) {
86 | if (_modules[6][c] != null) {
87 | continue;
88 | }
89 | _modules[6][c] = c % 2 == 0;
90 | }
91 | },
92 | setupPositionAdjustPattern = function () {
93 | var pos = QRUtil.getPatternPosition(_typeNumber);
94 |
95 | for (var i = 0; i < pos.length; i += 1) {
96 | for (var j = 0; j < pos.length; j += 1) {
97 | var row = pos[i];
98 | var col = pos[j];
99 |
100 | if (_modules[row][col] != null) {
101 | continue;
102 | }
103 |
104 | for (var r = -2; r <= 2; r += 1) {
105 | for (var c = -2; c <= 2; c += 1) {
106 | _modules[row + r][col + c] = r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0);
107 | }
108 | }
109 | }
110 | }
111 | },
112 | // TODO rm5 can be removed if we fix type to 5 (this method is called at 7 only)
113 | setupTypeNumber = function (test) {
114 | var bits = QRUtil.getBCHTypeNumber(_typeNumber);
115 |
116 | for (var i = 0; i < 18; i += 1) {
117 | var mod = !test && ((bits >> i) & 1) == 1;
118 | _modules[Math.floor(i / 3)][(i % 3) + _moduleCount - 8 - 3] = mod;
119 | }
120 |
121 | for (var i = 0; i < 18; i += 1) {
122 | var mod = !test && ((bits >> i) & 1) == 1;
123 | _modules[(i % 3) + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod;
124 | }
125 | },
126 | setupTypeInfo = function (test, maskPattern) {
127 | var data = (_errorCorrectLevel << 3) | maskPattern;
128 | var bits = QRUtil.getBCHTypeInfo(data);
129 |
130 | for (var i = 0; i < 15; i += 1) {
131 | let mod = !test && ((bits >> i) & 1) == 1;
132 |
133 | // vertical then horizontal
134 | _modules[i < 6 ? i : i < 8 ? i + 1 : _moduleCount - 15 + i][8] = mod;
135 | _modules[8][i < 8 ? _moduleCount - i - 1 : i < 9 ? 15 - i : 14 - i] = mod;
136 | }
137 |
138 | // fixed module
139 | _modules[_moduleCount - 8][8] = !test;
140 | },
141 | mapData = function (data, maskPattern) {
142 | var inc = -1,
143 | row = _moduleCount - 1,
144 | bitIndex = 7,
145 | byteIndex = 0,
146 | maskFunc = QRUtil.getMaskFunction(maskPattern);
147 |
148 | for (var col = _moduleCount - 1; col > 0; col -= 2) {
149 | if (col == 6) col -= 1;
150 |
151 | while (true) {
152 | for (var c = 0; c < 2; c += 1) {
153 | if (_modules[row][col - c] == null) {
154 | var dark = false;
155 |
156 | if (byteIndex < data.length) {
157 | dark = ((data[byteIndex] >>> bitIndex) & 1) == 1;
158 | }
159 |
160 | var mask = maskFunc(row, col - c);
161 |
162 | if (mask) {
163 | dark = !dark;
164 | }
165 |
166 | _modules[row][col - c] = dark;
167 | bitIndex -= 1;
168 |
169 | if (bitIndex == -1) {
170 | byteIndex += 1;
171 | bitIndex = 7;
172 | }
173 | }
174 | }
175 |
176 | row += inc;
177 |
178 | if (row < 0 || _moduleCount <= row) {
179 | row -= inc;
180 | inc = -inc;
181 | break;
182 | }
183 | }
184 | }
185 | },
186 | createBytes = function (buffer, rsBlocks) {
187 | var offset = 0,
188 | maxDcCount = 0,
189 | maxEcCount = 0,
190 | dcdata = new Array(rsBlocks.length),
191 | ecdata = new Array(rsBlocks.length);
192 |
193 | for (var r = 0; r < rsBlocks.length; r += 1) {
194 | var dcCount = rsBlocks[r].dataCount,
195 | ecCount = rsBlocks[r].totalCount - dcCount;
196 |
197 | maxDcCount = Math.max(maxDcCount, dcCount);
198 | maxEcCount = Math.max(maxEcCount, ecCount);
199 |
200 | dcdata[r] = new Array(dcCount);
201 |
202 | for (var i = 0; i < dcdata[r].length; i += 1) {
203 | dcdata[r][i] = 0xff & buffer.getBuffer()[i + offset];
204 | }
205 | offset += dcCount;
206 |
207 | var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount),
208 | rawPoly = qrPolynomial(dcdata[r], rsPoly.getLength() - 1),
209 | modPoly = rawPoly.mod(rsPoly);
210 |
211 | ecdata[r] = new Array(rsPoly.getLength() - 1);
212 | for (var i = 0; i < ecdata[r].length; i += 1) {
213 | var modIndex = i + modPoly.getLength() - ecdata[r].length;
214 | ecdata[r][i] = modIndex >= 0 ? modPoly.getAt(modIndex) : 0;
215 | }
216 | }
217 |
218 | var totalCodeCount = 0;
219 | for (var i = 0; i < rsBlocks.length; i += 1) {
220 | totalCodeCount += rsBlocks[i].totalCount;
221 | }
222 |
223 | var data = new Array(totalCodeCount);
224 | var index = 0;
225 |
226 | for (var i = 0; i < maxDcCount; i += 1) {
227 | for (var r = 0; r < rsBlocks.length; r += 1) {
228 | if (i < dcdata[r].length) {
229 | data[index] = dcdata[r][i];
230 | index += 1;
231 | }
232 | }
233 | }
234 |
235 | for (var i = 0; i < maxEcCount; i += 1) {
236 | for (var r = 0; r < rsBlocks.length; r += 1) {
237 | if (i < ecdata[r].length) {
238 | data[index] = ecdata[r][i];
239 | index += 1;
240 | }
241 | }
242 | }
243 |
244 | return data;
245 | },
246 | createData = function (typeNumber, errorCorrectLevel, dataList) {
247 | var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel),
248 | buffer = qrBitBuffer();
249 |
250 | for (var i = 0; i < dataList.length; i += 1) {
251 | var data = dataList[i];
252 | buffer.put(data.getMode(), 4);
253 | buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber));
254 | data.write(buffer);
255 | }
256 |
257 | // calc num max data.
258 | var totalDataCount = 0;
259 | for (var i = 0; i < rsBlocks.length; i += 1) {
260 | totalDataCount += rsBlocks[i].dataCount;
261 | }
262 |
263 | if (buffer.getLengthInBits() > totalDataCount * 8) {
264 | throw new Error("code length overflow. (" + buffer.getLengthInBits() + ">" + totalDataCount * 8 + ")");
265 | }
266 |
267 | // end code
268 | if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
269 | buffer.put(0, 4);
270 | }
271 |
272 | // padding
273 | while (buffer.getLengthInBits() % 8 != 0) {
274 | buffer.putBit(false);
275 | }
276 |
277 | // padding
278 | while (true) {
279 | if (buffer.getLengthInBits() >= totalDataCount * 8) {
280 | break;
281 | }
282 | buffer.put(PAD0, 8);
283 |
284 | if (buffer.getLengthInBits() >= totalDataCount * 8) {
285 | break;
286 | }
287 | buffer.put(PAD1, 8);
288 | }
289 |
290 | return createBytes(buffer, rsBlocks);
291 | };
292 |
293 | _this.addData = function (data) {
294 | var newData = qr8BitByte(data);
295 | _dataList.push(newData);
296 | _dataCache = null;
297 | };
298 |
299 | _this.isDark = function (row, col) {
300 | if (row < 0 || _moduleCount <= row || col < 0 || _moduleCount <= col) {
301 | throw new Error(row + "," + col);
302 | }
303 | return _modules[row][col];
304 | };
305 |
306 | _this.getModuleCount = function () {
307 | return _moduleCount;
308 | };
309 |
310 | _this.make = function () {
311 | makeImpl(false, getBestMaskPattern());
312 | };
313 |
314 | return _this;
315 | };
316 |
317 | //---------------------------------------------------------------------
318 | // qrcode.stringToBytes
319 | //---------------------------------------------------------------------
320 |
321 | // UTF-8 version
322 | qrcode.stringToBytes = function (s) {
323 | // http://stackoverflow.com/questions/18729405/how-to-convert-utf8-string-to-byte-array
324 | function toUTF8Array(str) {
325 | var utf8 = [];
326 | for (var i = 0; i < str.length; i++) {
327 | var charcode = str.charCodeAt(i);
328 | if (charcode < 0x80) utf8.push(charcode);
329 | else if (charcode < 0x800) {
330 | utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f));
331 | } else if (charcode < 0xd800 || charcode >= 0xe000) {
332 | utf8.push(0xe0 | (charcode >> 12), 0x80 | ((charcode >> 6) & 0x3f), 0x80 | (charcode & 0x3f));
333 | }
334 | // surrogate pair
335 | else {
336 | i++;
337 | // UTF-16 encodes 0x10000-0x10FFFF by
338 | // subtracting 0x10000 and splitting the
339 | // 20 bits of 0x0-0xFFFFF into two halves
340 | charcode = 0x10000 + (((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
341 | utf8.push(
342 | 0xf0 | (charcode >> 18),
343 | 0x80 | ((charcode >> 12) & 0x3f),
344 | 0x80 | ((charcode >> 6) & 0x3f),
345 | 0x80 | (charcode & 0x3f)
346 | );
347 | }
348 | }
349 | return utf8;
350 | }
351 | return toUTF8Array(s);
352 | };
353 |
354 | //---------------------------------------------------------------------
355 | // QRMode
356 | //---------------------------------------------------------------------
357 |
358 | var QRMode = {
359 | MODE_8BIT_BYTE: 1 << 2,
360 | };
361 |
362 | //---------------------------------------------------------------------
363 | // QRErrorCorrectLevel
364 | //---------------------------------------------------------------------
365 |
366 | var QRErrorCorrectLevel = {
367 | L: 1,
368 | M: 0,
369 | Q: 3,
370 | H: 2,
371 | };
372 |
373 | //---------------------------------------------------------------------
374 | // QRMaskPattern
375 | //---------------------------------------------------------------------
376 |
377 | var QRMaskPattern = {
378 | PATTERN000: 0,
379 | PATTERN001: 1,
380 | PATTERN010: 2,
381 | PATTERN011: 3,
382 | PATTERN100: 4,
383 | PATTERN101: 5,
384 | PATTERN110: 6,
385 | PATTERN111: 7,
386 | };
387 |
388 | //---------------------------------------------------------------------
389 | // QRUtil
390 | //---------------------------------------------------------------------
391 |
392 | var QRUtil = (function () {
393 | var PATTERN_POSITION_TABLE = [
394 | [],
395 | [6, 18],
396 | [6, 22],
397 | [6, 26],
398 | [6, 30],
399 | [6, 34],
400 | [6, 22, 38],
401 | [6, 24, 42],
402 | [6, 26, 46],
403 | [6, 28, 50],
404 | [6, 30, 54],
405 | [6, 32, 58],
406 | [6, 34, 62],
407 | [6, 26, 46, 66],
408 | [6, 26, 48, 70],
409 | [6, 26, 50, 74],
410 | [6, 30, 54, 78],
411 | [6, 30, 56, 82],
412 | [6, 30, 58, 86],
413 | [6, 34, 62, 90],
414 | [6, 28, 50, 72, 94],
415 | [6, 26, 50, 74, 98],
416 | [6, 30, 54, 78, 102],
417 | [6, 28, 54, 80, 106],
418 | [6, 32, 58, 84, 110],
419 | [6, 30, 58, 86, 114],
420 | [6, 34, 62, 90, 118],
421 | [6, 26, 50, 74, 98, 122],
422 | [6, 30, 54, 78, 102, 126],
423 | [6, 26, 52, 78, 104, 130],
424 | [6, 30, 56, 82, 108, 134],
425 | [6, 34, 60, 86, 112, 138],
426 | [6, 30, 58, 86, 114, 142],
427 | [6, 34, 62, 90, 118, 146],
428 | [6, 30, 54, 78, 102, 126, 150],
429 | [6, 24, 50, 76, 102, 128, 154],
430 | [6, 28, 54, 80, 106, 132, 158],
431 | [6, 32, 58, 84, 110, 136, 162],
432 | [6, 26, 54, 82, 110, 138, 166],
433 | [6, 30, 58, 86, 114, 142, 170],
434 | ],
435 | G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0),
436 | G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0),
437 | G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1),
438 | _this = {},
439 | getBCHDigit = function (data) {
440 | var digit = 0;
441 | while (data != 0) {
442 | digit += 1;
443 | data >>>= 1;
444 | }
445 | return digit;
446 | };
447 |
448 | _this.getBCHTypeInfo = function (data) {
449 | var d = data << 10;
450 | while (getBCHDigit(d) - getBCHDigit(G15) >= 0) {
451 | d ^= G15 << (getBCHDigit(d) - getBCHDigit(G15));
452 | }
453 | return ((data << 10) | d) ^ G15_MASK;
454 | };
455 |
456 | // TODO rm5 (see rm5 above)
457 | _this.getBCHTypeNumber = function (data) {
458 | var d = data << 12;
459 | while (getBCHDigit(d) - getBCHDigit(G18) >= 0) {
460 | d ^= G18 << (getBCHDigit(d) - getBCHDigit(G18));
461 | }
462 | return (data << 12) | d;
463 | };
464 |
465 | _this.getPatternPosition = function (typeNumber) {
466 | return PATTERN_POSITION_TABLE[typeNumber - 1];
467 | };
468 |
469 | _this.getMaskFunction = function (maskPattern) {
470 | switch (maskPattern) {
471 | case QRMaskPattern.PATTERN000:
472 | return function (i, j) {
473 | return (i + j) % 2 == 0;
474 | };
475 | case QRMaskPattern.PATTERN001:
476 | return function (i, j) {
477 | return i % 2 == 0;
478 | };
479 | case QRMaskPattern.PATTERN010:
480 | return function (i, j) {
481 | return j % 3 == 0;
482 | };
483 | case QRMaskPattern.PATTERN011:
484 | return function (i, j) {
485 | return (i + j) % 3 == 0;
486 | };
487 | case QRMaskPattern.PATTERN100:
488 | return function (i, j) {
489 | return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0;
490 | };
491 | case QRMaskPattern.PATTERN101:
492 | return function (i, j) {
493 | return ((i * j) % 2) + ((i * j) % 3) == 0;
494 | };
495 | case QRMaskPattern.PATTERN110:
496 | return function (i, j) {
497 | return (((i * j) % 2) + ((i * j) % 3)) % 2 == 0;
498 | };
499 | case QRMaskPattern.PATTERN111:
500 | return function (i, j) {
501 | return (((i * j) % 3) + ((i + j) % 2)) % 2 == 0;
502 | };
503 |
504 | default:
505 | throw new Error("bad maskPattern:" + maskPattern);
506 | }
507 | };
508 |
509 | _this.getErrorCorrectPolynomial = function (errorCorrectLength) {
510 | var a = qrPolynomial([1], 0);
511 | for (var i = 0; i < errorCorrectLength; i += 1) {
512 | a = a.multiply(qrPolynomial([1, QRMath.gexp(i)], 0));
513 | }
514 | return a;
515 | };
516 |
517 | _this.getLengthInBits = function (mode, type) {
518 | if (mode != QRMode.MODE_8BIT_BYTE || type < 1 || type > 40) throw new Error("mode: " + mode + "; type: " + type);
519 |
520 | return type < 10 ? 8 : 16;
521 | };
522 |
523 | _this.getLostPoint = function (qrcode) {
524 | var moduleCount = qrcode.getModuleCount(),
525 | lostPoint = 0;
526 |
527 | // LEVEL1
528 |
529 | for (var row = 0; row < moduleCount; row += 1) {
530 | for (var col = 0; col < moduleCount; col += 1) {
531 | var sameCount = 0,
532 | dark = qrcode.isDark(row, col);
533 |
534 | for (var r = -1; r <= 1; r += 1) {
535 | if (row + r < 0 || moduleCount <= row + r) {
536 | continue;
537 | }
538 |
539 | for (var c = -1; c <= 1; c += 1) {
540 | if (col + c < 0 || moduleCount <= col + c) {
541 | continue;
542 | }
543 |
544 | if (r == 0 && c == 0) {
545 | continue;
546 | }
547 |
548 | if (dark == qrcode.isDark(row + r, col + c)) {
549 | sameCount += 1;
550 | }
551 | }
552 | }
553 |
554 | if (sameCount > 5) {
555 | lostPoint += 3 + sameCount - 5;
556 | }
557 | }
558 | }
559 |
560 | // LEVEL2
561 |
562 | for (var row = 0; row < moduleCount - 1; row += 1) {
563 | for (var col = 0; col < moduleCount - 1; col += 1) {
564 | var count = 0;
565 | if (qrcode.isDark(row, col)) count += 1;
566 | if (qrcode.isDark(row + 1, col)) count += 1;
567 | if (qrcode.isDark(row, col + 1)) count += 1;
568 | if (qrcode.isDark(row + 1, col + 1)) count += 1;
569 | if (count == 0 || count == 4) {
570 | lostPoint += 3;
571 | }
572 | }
573 | }
574 |
575 | // LEVEL3
576 |
577 | for (var row = 0; row < moduleCount; row += 1) {
578 | for (var col = 0; col < moduleCount - 6; col += 1) {
579 | if (
580 | qrcode.isDark(row, col) &&
581 | !qrcode.isDark(row, col + 1) &&
582 | qrcode.isDark(row, col + 2) &&
583 | qrcode.isDark(row, col + 3) &&
584 | qrcode.isDark(row, col + 4) &&
585 | !qrcode.isDark(row, col + 5) &&
586 | qrcode.isDark(row, col + 6)
587 | ) {
588 | lostPoint += 40;
589 | }
590 | }
591 | }
592 |
593 | for (var col = 0; col < moduleCount; col += 1) {
594 | for (var row = 0; row < moduleCount - 6; row += 1) {
595 | if (
596 | qrcode.isDark(row, col) &&
597 | !qrcode.isDark(row + 1, col) &&
598 | qrcode.isDark(row + 2, col) &&
599 | qrcode.isDark(row + 3, col) &&
600 | qrcode.isDark(row + 4, col) &&
601 | !qrcode.isDark(row + 5, col) &&
602 | qrcode.isDark(row + 6, col)
603 | ) {
604 | lostPoint += 40;
605 | }
606 | }
607 | }
608 |
609 | // LEVEL4
610 |
611 | var darkCount = 0;
612 |
613 | for (var col = 0; col < moduleCount; col += 1) {
614 | for (var row = 0; row < moduleCount; row += 1) {
615 | if (qrcode.isDark(row, col)) {
616 | darkCount += 1;
617 | }
618 | }
619 | }
620 |
621 | var ratio = Math.abs((100 * darkCount) / moduleCount / moduleCount - 50) / 5;
622 | lostPoint += ratio * 10;
623 |
624 | return lostPoint;
625 | };
626 |
627 | return _this;
628 | })();
629 |
630 | //---------------------------------------------------------------------
631 | // QRMath
632 | //---------------------------------------------------------------------
633 |
634 | var QRMath = (function () {
635 | var EXP_TABLE = new Array(256),
636 | LOG_TABLE = new Array(256);
637 |
638 | // initialize tables
639 | for (var i = 0; i < 8; i += 1) {
640 | EXP_TABLE[i] = 1 << i;
641 | }
642 | for (var i = 8; i < 256; i += 1) {
643 | EXP_TABLE[i] = EXP_TABLE[i - 4] ^ EXP_TABLE[i - 5] ^ EXP_TABLE[i - 6] ^ EXP_TABLE[i - 8];
644 | }
645 | for (var i = 0; i < 255; i += 1) {
646 | LOG_TABLE[EXP_TABLE[i]] = i;
647 | }
648 |
649 | var _this = {};
650 |
651 | _this.glog = function (n) {
652 | if (n < 1) {
653 | throw new Error("glog(" + n + ")");
654 | }
655 |
656 | return LOG_TABLE[n];
657 | };
658 |
659 | _this.gexp = function (n) {
660 | while (n < 0) {
661 | n += 255;
662 | }
663 |
664 | while (n >= 256) {
665 | n -= 255;
666 | }
667 |
668 | return EXP_TABLE[n];
669 | };
670 |
671 | return _this;
672 | })();
673 |
674 | //---------------------------------------------------------------------
675 | // qrPolynomial
676 | //---------------------------------------------------------------------
677 |
678 | function qrPolynomial(num, shift) {
679 | if (typeof num.length == "undefined") {
680 | throw new Error(num.length + "/" + shift);
681 | }
682 |
683 | var _num = (function () {
684 | var offset = 0;
685 | while (offset < num.length && num[offset] == 0) {
686 | offset += 1;
687 | }
688 | var _num = new Array(num.length - offset + shift);
689 | for (var i = 0; i < num.length - offset; i += 1) {
690 | _num[i] = num[i + offset];
691 | }
692 | return _num;
693 | })();
694 |
695 | var _this = {};
696 |
697 | _this.getAt = function (index) {
698 | return _num[index];
699 | };
700 |
701 | _this.getLength = function () {
702 | return _num.length;
703 | };
704 |
705 | _this.multiply = function (e) {
706 | var num = new Array(_this.getLength() + e.getLength() - 1);
707 |
708 | for (var i = 0; i < _this.getLength(); i += 1) {
709 | for (var j = 0; j < e.getLength(); j += 1) {
710 | num[i + j] ^= QRMath.gexp(QRMath.glog(_this.getAt(i)) + QRMath.glog(e.getAt(j)));
711 | }
712 | }
713 |
714 | return qrPolynomial(num, 0);
715 | };
716 |
717 | _this.mod = function (e) {
718 | if (_this.getLength() - e.getLength() < 0) {
719 | return _this;
720 | }
721 |
722 | var ratio = QRMath.glog(_this.getAt(0)) - QRMath.glog(e.getAt(0));
723 |
724 | var num = new Array(_this.getLength());
725 | for (var i = 0; i < _this.getLength(); i += 1) {
726 | num[i] = _this.getAt(i);
727 | }
728 |
729 | for (var i = 0; i < e.getLength(); i += 1) {
730 | num[i] ^= QRMath.gexp(QRMath.glog(e.getAt(i)) + ratio);
731 | }
732 |
733 | // recursive call
734 | return qrPolynomial(num, 0).mod(e);
735 | };
736 |
737 | return _this;
738 | }
739 |
740 | //---------------------------------------------------------------------
741 | // QRRSBlock
742 | //---------------------------------------------------------------------
743 |
744 | var QRRSBlock = (function () {
745 | // TODO is it possible to generate this block with JS in let kB?
746 | var RS_BLOCK_TABLE = [
747 | // L
748 | // M
749 | // Q
750 | // H
751 |
752 | // 1
753 | [1, 26, 19],
754 | [1, 26, 16],
755 | [1, 26, 13],
756 | [1, 26, 9],
757 |
758 | // 2
759 | [1, 44, 34],
760 | [1, 44, 28],
761 | [1, 44, 22],
762 | [1, 44, 16],
763 |
764 | // 3
765 | [1, 70, 55],
766 | [1, 70, 44],
767 | [2, 35, 17],
768 | [2, 35, 13],
769 |
770 | // 4
771 | [1, 100, 80],
772 | [2, 50, 32],
773 | [2, 50, 24],
774 | [4, 25, 9],
775 |
776 | // 5
777 | [1, 134, 108],
778 | [2, 67, 43],
779 | [2, 33, 15, 2, 34, 16],
780 | [2, 33, 11, 2, 34, 12],
781 |
782 | // 6
783 | [2, 86, 68],
784 | [4, 43, 27],
785 | [4, 43, 19],
786 | [4, 43, 15],
787 |
788 | // 7
789 | [2, 98, 78],
790 | [4, 49, 31],
791 | [2, 32, 14, 4, 33, 15],
792 | [4, 39, 13, 1, 40, 14],
793 |
794 | // 8
795 | [2, 121, 97],
796 | [2, 60, 38, 2, 61, 39],
797 | [4, 40, 18, 2, 41, 19],
798 | [4, 40, 14, 2, 41, 15],
799 |
800 | // 9
801 | [2, 146, 116],
802 | [3, 58, 36, 2, 59, 37],
803 | [4, 36, 16, 4, 37, 17],
804 | [4, 36, 12, 4, 37, 13],
805 |
806 | // 10
807 | [2, 86, 68, 2, 87, 69],
808 | [4, 69, 43, 1, 70, 44],
809 | [6, 43, 19, 2, 44, 20],
810 | [6, 43, 15, 2, 44, 16],
811 |
812 | // 11
813 | [4, 101, 81],
814 | [1, 80, 50, 4, 81, 51],
815 | [4, 50, 22, 4, 51, 23],
816 | [3, 36, 12, 8, 37, 13],
817 |
818 | // 12
819 | [2, 116, 92, 2, 117, 93],
820 | [6, 58, 36, 2, 59, 37],
821 | [4, 46, 20, 6, 47, 21],
822 | [7, 42, 14, 4, 43, 15],
823 |
824 | // 13
825 | [4, 133, 107],
826 | [8, 59, 37, 1, 60, 38],
827 | [8, 44, 20, 4, 45, 21],
828 | [12, 33, 11, 4, 34, 12],
829 |
830 | // 14
831 | [3, 145, 115, 1, 146, 116],
832 | [4, 64, 40, 5, 65, 41],
833 | [11, 36, 16, 5, 37, 17],
834 | [11, 36, 12, 5, 37, 13],
835 |
836 | // 15
837 | [5, 109, 87, 1, 110, 88],
838 | [5, 65, 41, 5, 66, 42],
839 | [5, 54, 24, 7, 55, 25],
840 | [11, 36, 12, 7, 37, 13],
841 |
842 | // 16
843 | [5, 122, 98, 1, 123, 99],
844 | [7, 73, 45, 3, 74, 46],
845 | [15, 43, 19, 2, 44, 20],
846 | [3, 45, 15, 13, 46, 16],
847 |
848 | // 17
849 | [1, 135, 107, 5, 136, 108],
850 | [10, 74, 46, 1, 75, 47],
851 | [1, 50, 22, 15, 51, 23],
852 | [2, 42, 14, 17, 43, 15],
853 |
854 | // 18
855 | [5, 150, 120, 1, 151, 121],
856 | [9, 69, 43, 4, 70, 44],
857 | [17, 50, 22, 1, 51, 23],
858 | [2, 42, 14, 19, 43, 15],
859 |
860 | // 19
861 | [3, 141, 113, 4, 142, 114],
862 | [3, 70, 44, 11, 71, 45],
863 | [17, 47, 21, 4, 48, 22],
864 | [9, 39, 13, 16, 40, 14],
865 |
866 | // 20
867 | [3, 135, 107, 5, 136, 108],
868 | [3, 67, 41, 13, 68, 42],
869 | [15, 54, 24, 5, 55, 25],
870 | [15, 43, 15, 10, 44, 16],
871 |
872 | // 21
873 | [4, 144, 116, 4, 145, 117],
874 | [17, 68, 42],
875 | [17, 50, 22, 6, 51, 23],
876 | [19, 46, 16, 6, 47, 17],
877 |
878 | // 22
879 | [2, 139, 111, 7, 140, 112],
880 | [17, 74, 46],
881 | [7, 54, 24, 16, 55, 25],
882 | [34, 37, 13],
883 |
884 | // 23
885 | [4, 151, 121, 5, 152, 122],
886 | [4, 75, 47, 14, 76, 48],
887 | [11, 54, 24, 14, 55, 25],
888 | [16, 45, 15, 14, 46, 16],
889 |
890 | // 24
891 | [6, 147, 117, 4, 148, 118],
892 | [6, 73, 45, 14, 74, 46],
893 | [11, 54, 24, 16, 55, 25],
894 | [30, 46, 16, 2, 47, 17],
895 |
896 | // 25
897 | [8, 132, 106, 4, 133, 107],
898 | [8, 75, 47, 13, 76, 48],
899 | [7, 54, 24, 22, 55, 25],
900 | [22, 45, 15, 13, 46, 16],
901 |
902 | // 26
903 | [10, 142, 114, 2, 143, 115],
904 | [19, 74, 46, 4, 75, 47],
905 | [28, 50, 22, 6, 51, 23],
906 | [33, 46, 16, 4, 47, 17],
907 |
908 | // 27
909 | [8, 152, 122, 4, 153, 123],
910 | [22, 73, 45, 3, 74, 46],
911 | [8, 53, 23, 26, 54, 24],
912 | [12, 45, 15, 28, 46, 16],
913 |
914 | // 28
915 | [3, 147, 117, 10, 148, 118],
916 | [3, 73, 45, 23, 74, 46],
917 | [4, 54, 24, 31, 55, 25],
918 | [11, 45, 15, 31, 46, 16],
919 |
920 | // 29
921 | [7, 146, 116, 7, 147, 117],
922 | [21, 73, 45, 7, 74, 46],
923 | [1, 53, 23, 37, 54, 24],
924 | [19, 45, 15, 26, 46, 16],
925 |
926 | // 30
927 | [5, 145, 115, 10, 146, 116],
928 | [19, 75, 47, 10, 76, 48],
929 | [15, 54, 24, 25, 55, 25],
930 | [23, 45, 15, 25, 46, 16],
931 |
932 | // 31
933 | [13, 145, 115, 3, 146, 116],
934 | [2, 74, 46, 29, 75, 47],
935 | [42, 54, 24, 1, 55, 25],
936 | [23, 45, 15, 28, 46, 16],
937 |
938 | // 32
939 | [17, 145, 115],
940 | [10, 74, 46, 23, 75, 47],
941 | [10, 54, 24, 35, 55, 25],
942 | [19, 45, 15, 35, 46, 16],
943 |
944 | // 33
945 | [17, 145, 115, 1, 146, 116],
946 | [14, 74, 46, 21, 75, 47],
947 | [29, 54, 24, 19, 55, 25],
948 | [11, 45, 15, 46, 46, 16],
949 |
950 | // 34
951 | [13, 145, 115, 6, 146, 116],
952 | [14, 74, 46, 23, 75, 47],
953 | [44, 54, 24, 7, 55, 25],
954 | [59, 46, 16, 1, 47, 17],
955 |
956 | // 35
957 | [12, 151, 121, 7, 152, 122],
958 | [12, 75, 47, 26, 76, 48],
959 | [39, 54, 24, 14, 55, 25],
960 | [22, 45, 15, 41, 46, 16],
961 |
962 | // 36
963 | [6, 151, 121, 14, 152, 122],
964 | [6, 75, 47, 34, 76, 48],
965 | [46, 54, 24, 10, 55, 25],
966 | [2, 45, 15, 64, 46, 16],
967 |
968 | // 37
969 | [17, 152, 122, 4, 153, 123],
970 | [29, 74, 46, 14, 75, 47],
971 | [49, 54, 24, 10, 55, 25],
972 | [24, 45, 15, 46, 46, 16],
973 |
974 | // 38
975 | [4, 152, 122, 18, 153, 123],
976 | [13, 74, 46, 32, 75, 47],
977 | [48, 54, 24, 14, 55, 25],
978 | [42, 45, 15, 32, 46, 16],
979 |
980 | // 39
981 | [20, 147, 117, 4, 148, 118],
982 | [40, 75, 47, 7, 76, 48],
983 | [43, 54, 24, 22, 55, 25],
984 | [10, 45, 15, 67, 46, 16],
985 |
986 | // 40
987 | [19, 148, 118, 6, 149, 119],
988 | [18, 75, 47, 31, 76, 48],
989 | [34, 54, 24, 34, 55, 25],
990 | [20, 45, 15, 61, 46, 16],
991 | ];
992 |
993 | var qrRSBlock = function (totalCount, dataCount) {
994 | var _this = {};
995 | _this.totalCount = totalCount;
996 | _this.dataCount = dataCount;
997 | return _this;
998 | };
999 |
1000 | var _this = {};
1001 |
1002 | var getRsBlockTable = function (typeNumber, errorCorrectLevel) {
1003 | switch (errorCorrectLevel) {
1004 | case QRErrorCorrectLevel["L"]:
1005 | return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];
1006 | case QRErrorCorrectLevel["M"]:
1007 | return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];
1008 | case QRErrorCorrectLevel["Q"]:
1009 | return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];
1010 | case QRErrorCorrectLevel["H"]:
1011 | return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];
1012 | default:
1013 | return undefined;
1014 | }
1015 | };
1016 |
1017 | _this.getRSBlocks = function (typeNumber, errorCorrectLevel) {
1018 | var rsBlock = getRsBlockTable(typeNumber, errorCorrectLevel);
1019 |
1020 | if (typeof rsBlock == "undefined") {
1021 | throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel);
1022 | }
1023 |
1024 | var length = rsBlock.length / 3,
1025 | list = new Array();
1026 |
1027 | for (var i = 0; i < length; i += 1) {
1028 | var count = rsBlock[i * 3 + 0],
1029 | totalCount = rsBlock[i * 3 + 1],
1030 | dataCount = rsBlock[i * 3 + 2];
1031 |
1032 | for (var j = 0; j < count; j += 1) {
1033 | list.push(qrRSBlock(totalCount, dataCount));
1034 | }
1035 | }
1036 |
1037 | return list;
1038 | };
1039 |
1040 | return _this;
1041 | })();
1042 |
1043 | //---------------------------------------------------------------------
1044 | // qrBitBuffer
1045 | //---------------------------------------------------------------------
1046 |
1047 | var qrBitBuffer = function () {
1048 | var _buffer = new Array(),
1049 | _length = 0,
1050 | _this = {};
1051 |
1052 | _this.getBuffer = function () {
1053 | return _buffer;
1054 | };
1055 |
1056 | _this.getAt = function (index) {
1057 | var bufIndex = Math.floor(index / 8);
1058 | return ((_buffer[bufIndex] >>> (7 - (index % 8))) & 1) == 1;
1059 | };
1060 |
1061 | _this.put = function (num, length) {
1062 | for (var i = 0; i < length; i += 1) {
1063 | _this.putBit(((num >>> (length - i - 1)) & 1) == 1);
1064 | }
1065 | };
1066 |
1067 | _this.getLengthInBits = function () {
1068 | return _length;
1069 | };
1070 |
1071 | _this.putBit = function (bit) {
1072 | var bufIndex = Math.floor(_length / 8);
1073 | if (_buffer.length <= bufIndex) {
1074 | _buffer.push(0);
1075 | }
1076 |
1077 | if (bit) {
1078 | _buffer[bufIndex] |= 0x80 >>> _length % 8;
1079 | }
1080 |
1081 | _length += 1;
1082 | };
1083 |
1084 | return _this;
1085 | };
1086 |
1087 | //---------------------------------------------------------------------
1088 | // qr8BitByte
1089 | //---------------------------------------------------------------------
1090 |
1091 | var qr8BitByte = function (data) {
1092 | var _mode = QRMode.MODE_8BIT_BYTE,
1093 | _data = data,
1094 | _bytes = qrcode.stringToBytes(data),
1095 | _this = {};
1096 |
1097 | _this.getMode = function () {
1098 | return _mode;
1099 | };
1100 |
1101 | _this.getLength = function (buffer) {
1102 | return _bytes.length;
1103 | };
1104 |
1105 | _this.write = function (buffer) {
1106 | for (var i = 0; i < _bytes.length; i += 1) {
1107 | buffer.put(_bytes[i], 8);
1108 | }
1109 | };
1110 |
1111 | return _this;
1112 | };
1113 |
--------------------------------------------------------------------------------
/src/qrcode-strategy/core/utils.js:
--------------------------------------------------------------------------------
1 | import { qrcode } from ".";
2 |
3 | function createQRCode(text, level, version, quiet) {
4 | const qr = {};
5 | const vqr = qrcode(version, level);
6 | vqr.addData(text);
7 | vqr.make();
8 |
9 | quiet = quiet || 0;
10 |
11 | const qrModuleCount = vqr.getModuleCount()
12 | const quietModuleCount = vqr.getModuleCount() + 2 * quiet;
13 |
14 | function isDark(row, col) {
15 | row -= quiet;
16 | col -= quiet;
17 |
18 | if (row < 0 || row >= qrModuleCount || col < 0 || col >= qrModuleCount) {
19 | return false;
20 | }
21 | return vqr.isDark(row, col);
22 | }
23 |
24 | qr.text = text;
25 | qr.level = level;
26 | qr.version = version;
27 | qr.moduleCount = quietModuleCount;
28 | qr.isDark = isDark;
29 | return qr;
30 | }
31 |
32 | // Returns a minimal QR code for the given text starting with version `minVersion`.
33 | // Returns `undefined` if `text` is too long to be encoded in `maxVersion`.
34 | export function createMinQRCode(text, level, minVersion, maxVersion, quiet) {
35 | minVersion = Math.max(1, minVersion || 1);
36 | maxVersion = Math.min(40, maxVersion || 40);
37 | for (var version = minVersion; version <= maxVersion; version += 1) {
38 | try {
39 | return createQRCode(text, level, version, quiet);
40 | } catch (err) {}
41 | }
42 | return undefined;
43 | }
44 |
45 | // used when center is filled
46 | export function drawModuleRoundedDark(ctx, l, t, r, b, rad, nw, ne, se, sw) {
47 | //let moveTo = (x, y) => ctx.moveTo(Math.floor(x), Math.floor(y));
48 | if (nw) {
49 | ctx.moveTo(l + rad, t);
50 | } else {
51 | ctx.moveTo(l, t);
52 | }
53 |
54 | function lal(b, x0, y0, x1, y1, r0, r1) {
55 | if (b) {
56 | ctx.lineTo(x0 + r0, y0 + r1);
57 | ctx.arcTo(x0, y0, x1, y1, rad);
58 | } else {
59 | ctx.lineTo(x0, y0);
60 | }
61 | }
62 |
63 | lal(ne, r, t, r, b, -rad, 0);
64 | lal(se, r, b, l, b, 0, -rad);
65 | lal(sw, l, b, l, t, rad, 0);
66 | lal(nw, l, t, r, t, 0, rad);
67 | }
68 |
69 | // used when center is empty
70 | export function drawModuleRoundendLight(ctx, l, t, r, b, rad, nw, ne, se, sw) {
71 | function mlla(x, y, r0, r1) {
72 | ctx.moveTo(x + r0, y);
73 | ctx.lineTo(x, y);
74 | ctx.lineTo(x, y + r1);
75 | ctx.arcTo(x, y, x + r0, y, rad);
76 | }
77 |
78 | if (nw) mlla(l, t, rad, rad);
79 | if (ne) mlla(r, t, -rad, rad);
80 | if (se) mlla(r, b, -rad, -rad);
81 | if (sw) mlla(l, b, rad, -rad);
82 | }
83 |
84 | export function drawModuleRounded(qr, context, settings, left, top, width, row, col) {
85 | var isDark = qr.isDark,
86 | right = left + width,
87 | bottom = top + width,
88 | rowT = row - 1,
89 | rowB = row + 1,
90 | colL = col - 1,
91 | colR = col + 1,
92 | radius = Math.floor(Math.min(0.5, Math.max(0, settings.radius)) * width),
93 | center = isDark(row, col),
94 | northwest = isDark(rowT, colL),
95 | north = isDark(rowT, col),
96 | northeast = isDark(rowT, colR),
97 | east = isDark(row, colR),
98 | southeast = isDark(rowB, colR),
99 | south = isDark(rowB, col),
100 | southwest = isDark(rowB, colL),
101 | west = isDark(row, colL);
102 |
103 | left = Math.round(left);
104 | top = Math.round(top);
105 | right = Math.round(right);
106 | bottom = Math.round(bottom);
107 |
108 | if (center) {
109 | drawModuleRoundedDark(
110 | context,
111 | left,
112 | top,
113 | right,
114 | bottom,
115 | radius,
116 | !north && !west,
117 | !north && !east,
118 | !south && !east,
119 | !south && !west
120 | );
121 | } else {
122 | drawModuleRoundendLight(
123 | context,
124 | left,
125 | top,
126 | right,
127 | bottom,
128 | radius,
129 | north && west && northwest,
130 | north && east && northeast,
131 | south && east && southeast,
132 | south && west && southwest
133 | );
134 | }
135 | }
136 |
137 | export function drawModules(qr, context, settings) {
138 | var moduleCount = qr.moduleCount,
139 | moduleSize = settings.size / moduleCount,
140 | row,
141 | col;
142 |
143 | context.beginPath();
144 | for (row = 0; row < moduleCount; row += 1) {
145 | for (col = 0; col < moduleCount; col += 1) {
146 | var l = col * moduleSize,
147 | t = row * moduleSize,
148 | w = moduleSize;
149 |
150 | drawModuleRounded(qr, context, settings, l, t, w, row, col);
151 | }
152 | }
153 |
154 | setFill(context, settings.fill, settings.size);
155 | context.fill();
156 | }
157 |
158 | export function setFill(context, fill, size) {
159 | if (typeof fill === "string") {
160 | // solid color
161 | context.fillStyle = fill;
162 | return;
163 | }
164 | const type = fill["type"],
165 | position = fill["position"],
166 | colorStops = fill["colorStops"];
167 | let gradient;
168 | const absolutePosition = position.map((coordinate) => Math.round(coordinate * size));
169 | if (type === "linear-gradient") {
170 | gradient = context.createLinearGradient.apply(context, absolutePosition);
171 | } else if (type === "radial-gradient") {
172 | gradient = context.createRadialGradient.apply(context, absolutePosition);
173 | } else {
174 | throw new Error("Unsupported fill");
175 | }
176 | colorStops.forEach(([offset, color]) => {
177 | gradient.addColorStop(offset, color);
178 | });
179 | context.fillStyle = gradient;
180 | }
181 |
182 |
--------------------------------------------------------------------------------
/src/qrcode-strategy/index.ts:
--------------------------------------------------------------------------------
1 | import { HereProviderRequest, HereProviderResult } from "../types";
2 | import { HereStrategy } from "../strategies/HereStrategy";
3 | import QRCode, { QRSettings } from "./qrcode";
4 | import logo from "./logo";
5 |
6 | export { QRCode, QRSettings, logo };
7 |
8 | export interface QRCodeStrategyOptions extends Partial {
9 | element: HTMLElement;
10 | theme?: "dark" | "light";
11 | animate?: boolean;
12 | endpoint?: string;
13 | }
14 |
15 | export class QRCodeStrategy extends HereStrategy {
16 | private qrcode?: QRCode;
17 | readonly endpoint: string;
18 |
19 | constructor(public options: QRCodeStrategyOptions) {
20 | super();
21 | this.endpoint = options.endpoint ?? "https://my.herewallet.app/request";
22 | }
23 |
24 | get themeConfig() {
25 | return this.options.theme === "light" ? lightQR : darkQR;
26 | }
27 |
28 | async onRequested(id: string, request: HereProviderRequest) {
29 | this.qrcode = new QRCode({
30 | ...this.themeConfig,
31 | ...this.options,
32 | value: `${this.endpoint}/${id}`,
33 | });
34 |
35 | this.options.element.appendChild(this.qrcode.canvas);
36 | this.options.animate ? this.qrcode.animate() : this.qrcode.render();
37 | }
38 |
39 | close() {
40 | if (this.qrcode == null) return;
41 | this.options.element.removeChild(this.qrcode.canvas);
42 | this.qrcode?.stopAnimate();
43 | }
44 |
45 | async onApproving(result: HereProviderResult) {}
46 |
47 | async onFailed(result: HereProviderResult) {
48 | this.close();
49 | }
50 |
51 | async onSuccess(result: HereProviderResult) {
52 | this.close();
53 | }
54 | }
55 |
56 | export const darkQR: QRSettings = {
57 | value: "",
58 | radius: 0.8,
59 | ecLevel: "H",
60 | fill: {
61 | type: "linear-gradient",
62 | position: [0, 0, 1, 1],
63 | colorStops: [
64 | [0, "#2C3034"],
65 | [0.34, "#4F5256"],
66 | [1, "#2C3034"],
67 | ],
68 | },
69 | size: 256,
70 | withLogo: true,
71 | imageEcCover: 0.7,
72 | quiet: 1,
73 | };
74 |
75 | export const lightQR: QRSettings = {
76 | value: "",
77 | radius: 0.8,
78 | ecLevel: "H",
79 | fill: {
80 | type: "linear-gradient",
81 | position: [0.3, 0.3, 1, 1],
82 | colorStops: [
83 | [0, "#FDBF1C"],
84 | [1, "#FDA31C"],
85 | ],
86 | },
87 | size: 256,
88 | withLogo: true,
89 | imageEcCover: 0.7,
90 | quiet: 1,
91 | };
92 |
--------------------------------------------------------------------------------
/src/qrcode-strategy/logo.ts:
--------------------------------------------------------------------------------
1 | export default "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwYXRoIGQ9Ik0yNy4zOTYxIDQ0LjQ1NDNMMjcuNDg2NSA0NC40OTkyTDI3LjU4NzMgNDQuNDkzMUw1Ni4yNTAxIDQyLjc1MThMNTMuODE2OCA0Ny4xNDYzTDI0Ljc3NDggNDguOTEwNUw3LjYwNzIxIDQwLjM4MTRMMTAuMTA4IDM1Ljg2NTFMMjcuMzk2MSA0NC40NTQzWk0zMi45Mzk3IDI0LjM5MjVMMzIuMDI3MSAyMC4wNTkxTDM0LjM2NjQgMTUuNjk0M0wzNy41ODcyIDI2LjQ5NTJMMzIuOTM5NyAyNC4zOTI1Wk0yOC4xODQ1IDMzLjk1NjFMMTcuNTcxNiAzNC42ODkxTDkuMTk4NyAzMC41OUwxMS43MTggMjYuMDQzOEwyOC4xODQ1IDMzLjk1NjFaIiBmaWxsPSIjMkMzMDM0IiBzdHJva2U9IiNGREJGMUMiIHN0cm9rZS13aWR0aD0iMC43NTYxNDQiIC8+CiAgICA8cGF0aCBkPSJNNTYuOTEwMiA0Mi4zMzJMNTYuNzg1MiA0Mi40MzM2TDI3LjQ5NjMgNDQuMzYzOUw5Ljk1NDEgMzUuNTE5NVYzNS4zNjY1TDI5LjAxMjcgMzQuMTY5TDExLjUwNjMgMjUuNTQxOEwyMC4xMDMgMTguMjY4M0wzNy45MzYgMjcuMDI5NkwzNC40Mzk1IDE0LjcwN0w1MS40OCAyMy4wNTQ5TDU2LjkxMDIgNDIuMzMyWiIgZmlsbD0iI0ZEQkYxQyIgLz4KPC9zdmc+Cg==";
2 |
--------------------------------------------------------------------------------
/src/qrcode-strategy/qrcode.ts:
--------------------------------------------------------------------------------
1 | import { createMinQRCode, drawModules } from "./core/utils";
2 | import logo from "./logo";
3 |
4 | interface QR {
5 | moduleCount: number;
6 | isDark: (row: number, col: number) => boolean;
7 | }
8 |
9 | export interface QRSettings {
10 | ecLevel: "L" | "M" | "Q" | "H";
11 | value: string;
12 | radius: number;
13 | fill: any;
14 | imageEcCover?: number;
15 | minVersion?: number;
16 | maxVersion?: number;
17 | quiet?: number;
18 | background?: string;
19 | withLogo: boolean;
20 | logo?: HTMLImageElement;
21 | size: number;
22 | }
23 |
24 | const logoImage = new Image();
25 | logoImage.src = logo;
26 |
27 | export class QRCodeLogo {
28 | private quietModuleCount = 0;
29 | private moduleSize = 0;
30 | private dataPixels = 0;
31 | private ratio = 0;
32 | private area = 0;
33 |
34 | private imageModuleWidth = 0;
35 | private imageModuleHeight = 0;
36 | private imageModuleLeft = 0;
37 | private imageModuleTop = 0;
38 | private imageFloatModuleWidth = 0;
39 | private imageFloatModuleHeight = 0;
40 | private imageLeft = 0;
41 | private imageTop = 0;
42 |
43 | public isLoaded = false;
44 |
45 | constructor(readonly settings: QRSettings, readonly qr: QR, public readonly img = logoImage) {
46 | this.img.addEventListener("load", () => this.process());
47 | if (this.img.width > 0) {
48 | this.process();
49 | }
50 | }
51 |
52 | private process() {
53 | const quiet = this.settings.quiet ?? 0;
54 | const imageEcCover = this.settings.imageEcCover ?? 0.5;
55 |
56 | this.isLoaded = true;
57 | this.quietModuleCount = this.qr.moduleCount - quiet * 2;
58 | this.moduleSize = this.settings.size / this.quietModuleCount;
59 | this.dataPixels = this.quietModuleCount * this.quietModuleCount - (49 * 3 + 25);
60 | this.ratio = this.img.width / this.img.height;
61 |
62 | const quality = {
63 | L: 0.07,
64 | M: 0.15,
65 | Q: 0.25,
66 | H: 0.3,
67 | };
68 |
69 | this.area = (quality[this.settings.ecLevel] * imageEcCover * this.dataPixels) | 0;
70 |
71 | this.imageModuleWidth = Math.min(this.quietModuleCount, Math.sqrt(this.area * this.ratio) | 0);
72 | this.imageModuleHeight = (this.imageModuleWidth / this.ratio) | 0;
73 | if (this.imageModuleHeight > this.quietModuleCount) {
74 | this.imageModuleHeight = this.quietModuleCount;
75 | this.imageModuleWidth = (this.imageModuleHeight * this.ratio) | 0;
76 | }
77 |
78 | this.imageModuleLeft = (this.qr.moduleCount / 2 - this.imageModuleWidth / 2) | 0;
79 | this.imageModuleTop = (this.qr.moduleCount / 2 - this.imageModuleHeight / 2) | 0;
80 |
81 | this.imageFloatModuleWidth = Math.min(this.imageModuleWidth, this.imageModuleHeight * this.ratio) - quiet;
82 | this.imageFloatModuleHeight = Math.min(this.imageModuleHeight, this.imageModuleWidth / this.ratio) - quiet;
83 | this.imageLeft = this.imageModuleLeft + (this.imageModuleWidth - this.imageFloatModuleWidth) / 2 - quiet;
84 | this.imageTop = this.imageModuleTop + (this.imageModuleHeight - this.imageFloatModuleHeight) / 2 - quiet;
85 |
86 | const isDark = this.qr.isDark;
87 | this.qr.isDark = (row, col) => {
88 | if (this.isContains(row, col)) return false;
89 | return isDark(row, col);
90 | };
91 | }
92 |
93 | render(context: CanvasRenderingContext2D) {
94 | context.drawImage(
95 | this.img,
96 | this.imageLeft * this.moduleSize,
97 | this.imageTop * this.moduleSize,
98 | this.imageFloatModuleWidth * this.moduleSize,
99 | this.imageFloatModuleHeight * this.moduleSize
100 | );
101 | }
102 |
103 | isContains(row: number, col: number) {
104 | return (
105 | this.imageModuleLeft <= col &&
106 | col < this.imageModuleLeft + this.imageModuleWidth &&
107 | this.imageModuleTop <= row &&
108 | row < this.imageModuleTop + this.imageModuleHeight
109 | );
110 | }
111 | }
112 |
113 | class QRCode {
114 | public readonly canvas = document.createElement("canvas");
115 | public readonly ctx: CanvasRenderingContext2D;
116 | public readonly settings: QRSettings;
117 | public readonly logo?: QRCodeLogo;
118 | public readonly qr: QR;
119 |
120 | private rafHandler = 0;
121 |
122 | constructor(settings: QRSettings) {
123 | this.qr = createMinQRCode(
124 | settings.value,
125 | settings.ecLevel ?? "H",
126 | settings.minVersion ?? 1,
127 | settings.maxVersion ?? 40,
128 | settings.quiet ?? 0
129 | ) as QR;
130 |
131 | this.settings = settings;
132 | this.ctx = this.canvas.getContext("2d")!;
133 |
134 | this.canvas.style.width = settings.size + "px";
135 | this.canvas.style.height = settings.size + "px";
136 |
137 | settings.size = settings.size * 4;
138 | this.canvas.width = settings.size;
139 | this.canvas.height = settings.size;
140 |
141 | if (settings.withLogo) {
142 | this.logo = new QRCodeLogo(settings, this.qr, settings.logo);
143 | this.logo.img.addEventListener("load", () => this.render());
144 | }
145 | }
146 |
147 | render() {
148 | if (this.settings.background) {
149 | this.ctx.fillStyle = this.settings.background;
150 | this.ctx.fillRect(0, 0, this.settings.size, this.settings.size);
151 | } else {
152 | this.ctx.clearRect(0, 0, this.settings.size, this.settings.size);
153 | }
154 |
155 | drawModules(this.qr, this.ctx, this.settings);
156 | this.logo?.render(this.ctx);
157 | }
158 |
159 | animate = () => {
160 | this.render();
161 | this.rafHandler = requestAnimationFrame(this.animate);
162 | };
163 |
164 | stopAnimate() {
165 | cancelAnimationFrame(this.rafHandler);
166 | }
167 | }
168 |
169 | export default QRCode;
170 |
--------------------------------------------------------------------------------
/src/storage/HereKeyStore.ts:
--------------------------------------------------------------------------------
1 | import { KeyPair } from "@near-js/crypto";
2 | import { KeyStore } from "@near-js/keystores";
3 | import { HereJsonStorage, StateStorage } from "./JSONStorage";
4 |
5 | export interface HereAuthStorage extends KeyStore {
6 | setActiveAccount(network: string, id: string): Promise;
7 | getActiveAccount(network: string): Promise;
8 | }
9 |
10 | export class HereKeyStore implements HereAuthStorage {
11 | constructor(private storage: HereJsonStorage = new StateStorage()) {}
12 |
13 | async setActiveAccount(network: string, id: string) {
14 | const state = await this.storage.getState(network);
15 | state.activeAccount = id;
16 | this.storage.setState(network, state);
17 | }
18 |
19 | async setKey(networkId: string, accountId: string, keyPair: KeyPair) {
20 | const state = await this.storage.getState(networkId);
21 | state.accounts[accountId] = keyPair.toString();
22 | this.storage.setState(networkId, state);
23 | }
24 |
25 | async getAccounts(network: string) {
26 | const state = await this.storage.getState(network);
27 | return Object.keys(state.accounts);
28 | }
29 |
30 | async getActiveAccount(network: string) {
31 | const state = await this.storage.getState(network);
32 | return state.activeAccount;
33 | }
34 |
35 | async getKey(networkId: string, accountId: string) {
36 | const state = await this.storage.getState(networkId);
37 | const privateKey = state.accounts[accountId];
38 | if (privateKey == null) throw Error(`For ${accountId} in ${networkId} network key not found`);
39 | const keyPair = KeyPair.fromString(privateKey);
40 | return keyPair;
41 | }
42 |
43 | async removeKey(networkId: string, accountId: string) {
44 | let state = await this.storage.getState(networkId);
45 | if (state.activeAccount === accountId) {
46 | state.activeAccount = null;
47 | }
48 |
49 | delete state.accounts[accountId];
50 | this.storage.setState(networkId, state);
51 | }
52 |
53 | async getNetworks() {
54 | let state = await this.storage.getFullState();
55 | return Object.keys(state.accounts);
56 | }
57 |
58 | async clear() {
59 | await this.storage.clear();
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/storage/JSONStorage.ts:
--------------------------------------------------------------------------------
1 | export interface HereJsonStorage {
2 | setState(network: string, state: AuthState): Promise;
3 | getFullState(): Promise>;
4 | getState(network: string): Promise;
5 | clear(): Promise;
6 | }
7 |
8 | export interface AuthState {
9 | activeAccount: string | null;
10 | accounts: Record;
11 | }
12 |
13 | const mockStorage = {
14 | getItem(k: string): string | undefined | null {
15 | return null;
16 | },
17 |
18 | setItem(k: string, v: any) {},
19 | removeItem(k: string) {},
20 | };
21 |
22 | export class StateStorage implements HereJsonStorage {
23 | private readonly dataKey = `herewallet:keystore`;
24 | private readonly storage = typeof window !== "undefined" ? window.localStorage : mockStorage;
25 | constructor() {}
26 |
27 | async setState(network: string, state: AuthState) {
28 | const data = await this.getFullState();
29 | data[network] = state;
30 | this.storage.setItem(this.dataKey, JSON.stringify(data));
31 | }
32 |
33 | async getFullState(): Promise> {
34 | try {
35 | return JSON.parse(this.storage.getItem(this.dataKey)!) || {};
36 | } catch {
37 | return {};
38 | }
39 | }
40 |
41 | async getState(network: string): Promise {
42 | const json = await this.getFullState();
43 | return json[network] || { activeAccount: null, accounts: {} };
44 | }
45 |
46 | async clear() {
47 | this.storage.removeItem(this.dataKey);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/strategies/HereStrategy.ts:
--------------------------------------------------------------------------------
1 | import { HereStrategyRequest, HereProviderError, HereProviderResult, HereProviderStatus, HereWalletProtocol, HereProviderRequest } from "../types";
2 | import { createRequest, getResponse, deleteRequest, proxyApi, getRequest } from "../helpers/proxyMethods";
3 |
4 | export { createRequest, getResponse, deleteRequest, proxyApi, getRequest };
5 |
6 | export class HereStrategy {
7 | public wallet?: HereWalletProtocol;
8 | async connect(wallet: HereWalletProtocol) {
9 | this.wallet = wallet;
10 | }
11 |
12 | async onInitialized() {}
13 | async onRequested(id: string, request: HereProviderRequest, reject: (p?: string | undefined) => void) {}
14 | async onApproving(result: HereProviderResult) {}
15 | async onSuccess(result: HereProviderResult) {}
16 | async onFailed(result: HereProviderResult) {}
17 |
18 | async request(conf: HereStrategyRequest) {
19 | let { request, disableCleanupRequest, id, signal, ...delegate } = conf;
20 | if (id != null) request = await getRequest(id, signal);
21 | else id = await createRequest(request, signal);
22 |
23 | return new Promise((resolve, reject: (e: HereProviderError) => void) => {
24 | let fallbackHttpTimer: NodeJS.Timeout | number | null = null;
25 | const clear = async () => {
26 | fallbackHttpTimer = -1;
27 | clearInterval(fallbackHttpTimer);
28 | if (disableCleanupRequest !== true) {
29 | await deleteRequest(id!);
30 | }
31 | };
32 |
33 | const processApprove = (data: HereProviderResult) => {
34 | switch (data.status) {
35 | case HereProviderStatus.APPROVING:
36 | this.onApproving(data);
37 | return;
38 |
39 | case HereProviderStatus.FAILED:
40 | clear();
41 | reject(new HereProviderError(data.payload));
42 | this.onFailed(data);
43 | return;
44 |
45 | case HereProviderStatus.SUCCESS:
46 | clear();
47 | resolve(data);
48 | this.onSuccess(data);
49 | return;
50 | }
51 | };
52 |
53 | const rejectAction = (payload?: string) => {
54 | processApprove({
55 | type: request.selector?.type || "web",
56 | status: HereProviderStatus.FAILED,
57 | payload,
58 | });
59 | };
60 |
61 | this.onRequested(id!, request, rejectAction);
62 | signal?.addEventListener("abort", () => rejectAction());
63 |
64 | const setupTimer = () => {
65 | if (fallbackHttpTimer === -1) {
66 | return;
67 | }
68 |
69 | fallbackHttpTimer = setTimeout(async () => {
70 | try {
71 | const data = await getResponse(id!);
72 | if (fallbackHttpTimer === -1) return;
73 | processApprove(data);
74 | setupTimer();
75 | } catch (e) {
76 | const status = HereProviderStatus.FAILED;
77 | const error = e instanceof Error ? e : undefined;
78 | const payload = error?.message;
79 |
80 | clear();
81 | reject(new HereProviderError(payload, error));
82 | this.onFailed({ type: request.selector?.type || "web", status, payload });
83 | }
84 | }, 3000);
85 | };
86 |
87 | setupTimer();
88 | });
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/strategies/InjectedStrategy.ts:
--------------------------------------------------------------------------------
1 | import uuid4 from "uuid4";
2 | import { KeyPairEd25519 } from "@near-js/crypto";
3 | import { HereProviderResult, HereProviderStatus, HereStrategyRequest, HereWalletProtocol } from "../types";
4 | import { waitInjectedHereWallet } from "../helpers/waitInjected";
5 | import { HereStrategy } from "./HereStrategy";
6 |
7 | export class InjectedStrategy extends HereStrategy {
8 | async connect(wallet: HereWalletProtocol): Promise {
9 | if (typeof window === "undefined") return Promise.resolve();
10 |
11 | this.wallet = wallet;
12 | const injected = await waitInjectedHereWallet;
13 | if (injected == null) return;
14 |
15 | await this.wallet.authStorage.setKey(injected.network, injected.accountId, KeyPairEd25519.fromRandom());
16 | await this.wallet.authStorage.setActiveAccount(injected.network, injected.accountId);
17 | }
18 |
19 | async request(conf: HereStrategyRequest) {
20 | if (typeof window === "undefined") return Promise.reject("SSR");
21 |
22 | if (window.hotExtension) {
23 | return await window.hotExtension.request("near:hereConnect", conf.request);
24 | }
25 |
26 | return new Promise((resolve) => {
27 | const id = uuid4();
28 | const handler = (e: any) => {
29 | if (e.data.id !== id) return;
30 | if (e.data.status === HereProviderStatus.SUCCESS || e.data.status === HereProviderStatus.FAILED) {
31 | window?.removeEventListener("message", handler);
32 | return resolve(e.data);
33 | }
34 | };
35 |
36 | window?.parent.postMessage({ $here: true, ...conf.request, id }, "*");
37 | window?.addEventListener("message", handler);
38 | });
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/strategies/TelegramAppStrategy.ts:
--------------------------------------------------------------------------------
1 | import { KeyPair, KeyPairEd25519 } from "@near-js/crypto";
2 | import { baseDecode, baseEncode } from "@near-js/utils";
3 |
4 | import { HereProviderStatus, HereStrategyRequest, HereWalletProtocol } from "../types";
5 | import { computeRequestId, proxyApi } from "../helpers/proxyMethods";
6 | import { HereStrategy, getResponse } from "./HereStrategy";
7 | import { getDeviceId } from "../helpers/utils";
8 |
9 | export class TelegramAppStrategy extends HereStrategy {
10 | constructor(public appId = "herewalletbot/app", public walletId = "herewalletbot/app") {
11 | super();
12 | }
13 |
14 | async connect(wallet: HereWalletProtocol): Promise {
15 | if (typeof window === "undefined") return;
16 | this.wallet = wallet;
17 |
18 | const startapp = window?.Telegram?.WebApp?.initDataUnsafe?.start_param || "";
19 | window?.Telegram?.WebApp.ready?.();
20 |
21 | if (startapp.startsWith("hot")) {
22 | let requestId = startapp.split("-").pop() || "";
23 | requestId = Buffer.from(baseDecode(requestId)).toString("utf8");
24 |
25 | const requestPending = localStorage.getItem(`__telegramPendings:${requestId}`);
26 | if (requestPending == null) return;
27 |
28 | const data: any = await getResponse(requestId);
29 | if (data.status !== HereProviderStatus.SUCCESS) {
30 | localStorage.removeItem(`__telegramPendings:${requestId}`);
31 | return;
32 | }
33 |
34 | if (data.type === "sign") {
35 | await this.wallet.authStorage.setKey("mainnet", data.account_id!, KeyPairEd25519.fromRandom());
36 | await this.wallet.authStorage.setActiveAccount("mainnet", data.account_id!);
37 | }
38 |
39 | try {
40 | const pending = JSON.parse(requestPending);
41 | if (pending.privateKey) {
42 | await this.wallet.authStorage.setKey("mainnet", data.account_id!, KeyPair.fromString(pending.privateKey));
43 | await this.wallet.authStorage.setActiveAccount("mainnet", data.account_id!);
44 | }
45 |
46 | const url = new URL(location.origin + (pending.callbackUrl || ""));
47 | url.searchParams.set("payload", data.result!);
48 |
49 | localStorage.removeItem(`__telegramPendings:${requestId}`);
50 | location.assign(url.toString());
51 | } catch (e) {
52 | const url = new URL(location.href);
53 | url.searchParams.set("payload", data.result!);
54 |
55 | localStorage.removeItem(`__telegramPendings:${requestId}`);
56 | location.assign(url.toString());
57 | }
58 | }
59 | }
60 |
61 | async request(conf: HereStrategyRequest): Promise {
62 | if (typeof window === "undefined") return;
63 |
64 | conf.request.telegramApp = this.appId;
65 | conf.request.callbackUrl = "";
66 |
67 | const { requestId, query } = await computeRequestId(conf.request);
68 | const res = await fetch(`${proxyApi}/${requestId}/request`, {
69 | method: "POST",
70 | body: JSON.stringify({ topic_id: getDeviceId(), data: query }),
71 | headers: { "content-type": "application/json" },
72 | signal: conf.signal,
73 | });
74 |
75 | if (res.ok === false) {
76 | throw Error(await res.text());
77 | }
78 |
79 | localStorage.setItem(
80 | `__telegramPendings:${requestId}`,
81 | JSON.stringify({ callbackUrl: conf.callbackUrl, privateKey: conf.accessKey?.toString() })
82 | );
83 |
84 | this.onRequested(requestId);
85 | }
86 |
87 | async onRequested(id: string) {
88 | if (typeof window === "undefined") return;
89 |
90 | id = baseEncode(id);
91 | window?.Telegram?.WebApp?.openTelegramLink(`https://t.me/${this.walletId}?startapp=h4n-${id}`);
92 | window?.Telegram?.WebApp?.close();
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/strategies/WidgetStrategy.ts:
--------------------------------------------------------------------------------
1 | import { HereProviderRequest, HereProviderResult, HereWalletProtocol } from "../types";
2 | import { HereStrategy } from "./HereStrategy";
3 |
4 | const createIframe = (widget: string) => {
5 | const connector = document.createElement("iframe");
6 | connector.src = widget;
7 | connector.allow = "usb";
8 | connector.style.border = "none";
9 | connector.style.zIndex = "10000";
10 | connector.style.position = "fixed";
11 | connector.style.display = "none";
12 | connector.style.top = "0";
13 | connector.style.left = "0";
14 | connector.style.width = "100%";
15 | connector.style.height = "100%";
16 | document.body.appendChild(connector);
17 | return connector;
18 | };
19 |
20 | export const defaultUrl = "https://my.herewallet.app/connector/index.html";
21 |
22 | export class WidgetStrategy extends HereStrategy {
23 | private static connector?: HTMLIFrameElement;
24 | private static isLoaded = false;
25 |
26 | private messageHandler?: (event: MessageEvent) => void;
27 | readonly options: { lazy: boolean; widget: string };
28 |
29 | constructor(options: string | { lazy?: boolean; widget?: string } = { widget: defaultUrl, lazy: false }) {
30 | super();
31 |
32 | this.options = {
33 | lazy: typeof options === "object" ? options.lazy || false : false,
34 | widget: typeof options === "string" ? options : options.widget || defaultUrl,
35 | };
36 |
37 | if (!this.options.lazy) {
38 | this.initIframe();
39 | }
40 | }
41 |
42 | initIframe() {
43 | if (WidgetStrategy.connector == null) {
44 | WidgetStrategy.connector = createIframe(this.options.widget);
45 | WidgetStrategy.connector.addEventListener("load", () => {
46 | WidgetStrategy.isLoaded = true;
47 | });
48 | }
49 |
50 | return WidgetStrategy.connector;
51 | }
52 |
53 | async onRequested(id: string, request: HereProviderRequest, reject: (p?: string) => void) {
54 | const iframe = this.initIframe();
55 | iframe.style.display = "block";
56 |
57 | const loadHandler = () => {
58 | WidgetStrategy.connector?.removeEventListener("load", loadHandler);
59 | WidgetStrategy.connector?.contentWindow?.postMessage(
60 | JSON.stringify({ type: "request", payload: { id, request } }),
61 | new URL(this.options.widget).origin
62 | );
63 | };
64 |
65 | if (WidgetStrategy.isLoaded) loadHandler();
66 | else iframe.addEventListener("load", loadHandler);
67 |
68 | this.messageHandler = (event: MessageEvent) => {
69 | try {
70 | if (event.origin !== new URL(this.options.widget).origin) return;
71 | if (JSON.parse(event.data).type === "reject") reject();
72 | } catch {}
73 | };
74 |
75 | window?.addEventListener("message", this.messageHandler);
76 | }
77 |
78 | postMessage(data: object) {
79 | const iframe = this.initIframe();
80 | const args = JSON.stringify(data);
81 | const origin = new URL(this.options.widget).origin;
82 | iframe.contentWindow?.postMessage(args, origin);
83 | }
84 |
85 | async onApproving() {
86 | this.postMessage({ type: "approving" });
87 | }
88 |
89 | async onSuccess(request: HereProviderResult) {
90 | console.log(request);
91 | this.postMessage({ type: "result", payload: { request } });
92 | this.close();
93 | }
94 |
95 | async onFailed(request: HereProviderResult) {
96 | this.postMessage({ type: "result", payload: { request } });
97 | this.close();
98 | }
99 |
100 | close() {
101 | if (this.messageHandler) {
102 | window?.removeEventListener("message", this.messageHandler);
103 | this.messageHandler = undefined;
104 | }
105 |
106 | if (WidgetStrategy.connector) {
107 | WidgetStrategy.connector.style.display = "none";
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/strategies/WindowStrategy.ts:
--------------------------------------------------------------------------------
1 | import { HereProviderRequest } from "../types";
2 | import { HereStrategy } from "./HereStrategy";
3 |
4 | export class WindowStrategy extends HereStrategy {
5 | constructor(readonly endpoint = "https://my.herewallet.app") {
6 | super();
7 | }
8 |
9 | signWindow: Window | null = null;
10 | unloadHandler?: () => void;
11 | timerHandler?: NodeJS.Timeout;
12 |
13 | async onInitialized() {
14 | if (this.signWindow) return;
15 |
16 | const left = window.innerWidth / 2 - 420 / 2;
17 | const top = window.innerHeight / 2 - 700 / 2;
18 | this.signWindow = window.open(`${this.endpoint}/loading`, "_blank", `popup=1,width=420,height=700,top=${top},left=${left}`);
19 | }
20 |
21 | async onRequested(id: string, request: HereProviderRequest, reject: (p?: string) => void) {
22 | if (this.signWindow == null) return;
23 |
24 | this.unloadHandler = () => this.signWindow?.close();
25 | window.addEventListener("beforeunload", this.unloadHandler);
26 |
27 | this.signWindow.location = `${this.endpoint}/request/${id}`;
28 | this.timerHandler = setInterval(() => {
29 | if (this.signWindow?.closed) reject("CLOSED");
30 | }, 1000);
31 | }
32 |
33 | close() {
34 | clearInterval(this.timerHandler);
35 | this.signWindow?.close();
36 | this.signWindow = null;
37 |
38 | if (this.unloadHandler) {
39 | window.removeEventListener("beforeunload", this.unloadHandler);
40 | }
41 | }
42 |
43 | async onFailed() {
44 | this.close();
45 | }
46 |
47 | async onSuccess() {
48 | this.close();
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/telegramEthereumProvider.ts:
--------------------------------------------------------------------------------
1 | import uuid4 from "uuid4";
2 | import { waitInjectedHereWallet } from "./helpers/waitInjected";
3 |
4 | const promises: Record = {};
5 | const request = (type: string, args?: any) => {
6 | return new Promise((resolve, reject) => {
7 | const id = uuid4();
8 | window?.parent.postMessage({ type, id, args }, "*");
9 | promises[id] = { resolve, reject };
10 | });
11 | };
12 |
13 | const hereWalletProvider = {
14 | on() {},
15 | isHereWallet: true,
16 | isConnected: () => true,
17 | request: (data: any): Promise => request("ethereum", data),
18 | };
19 |
20 | async function announceProvider() {
21 | if (typeof window === "undefined") return;
22 | const injected = await waitInjectedHereWallet;
23 | if (injected == null) return;
24 |
25 | window?.dispatchEvent(
26 | new CustomEvent("eip6963:announceProvider", {
27 | detail: Object.freeze({
28 | provider: hereWalletProvider,
29 | info: {
30 | uuid: uuid4(),
31 | name: "HERE Wallet",
32 | icon: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTUwIiBoZWlnaHQ9IjU1MCIgdmlld0JveD0iMCAwIDU1MCA1NTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSI1NTAiIGhlaWdodD0iNTUwIiByeD0iMTIwIiBmaWxsPSIjRjNFQkVBIj48L3JlY3Q+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMjcyLjA0NiAxODMuNTM4TDI5My43ODggMTQzTDMyMi4yODggMjM4LjVMMjc5LjU1OCAyMTkuMTgyTDI3Mi4wNDYgMTgzLjUzOFpNMTE4LjI4OCAyMjZMOTYuMTg0IDI2NS44NTdMMTYzLjc2OSAyOTguOTJMMjU2Ljc4OCAyOTIuNUwxMTguMjg4IDIyNlpNMTA1Ljk2OSAzMDEuMTU4TDg0IDM0MC44MDNMMjE4LjkzNyA0MDcuNzkxTDQ0My44MDcgMzk0LjE0MUw0NjUuNzc2IDM1NC40OTZMMjQwLjkwNiAzNjguMTQ3TDEwNS45NjkgMzAxLjE1OFoiIGZpbGw9IiMyQzMwMzQiPjwvcGF0aD4KPHBhdGggZD0iTTQ2NS43ODggMzU0LjVMMjQwLjk4MiAzNjguMTUzTDEwNC44ODcgMzAxLjAwNUwyNTIuMjU5IDI5Mi4wODhMMTE4LjI4OCAyMjZMMTg0LjA3NiAxNzAuMjgyTDMyMC41NDcgMjM3LjM5N0wyOTMuNzg5IDE0My4wMDFMNDI0LjE5NSAyMDYuOTQ5TDQ2NS43ODggMzU0LjVaIiBmaWxsPSIjRkRCRjFDIj48L3BhdGg+Cjwvc3ZnPg==",
33 | rdns: "app.herewallet.my",
34 | },
35 | }),
36 | })
37 | );
38 | }
39 |
40 | if (typeof window !== "undefined") {
41 | window?.addEventListener("message", (e) => {
42 | if (e.data.type !== "ethereum") return;
43 | if (e.data.isSuccess) return promises[e.data.id]?.resolve(e.data.result);
44 | promises[e.data.id]?.reject(e.data.result);
45 | });
46 |
47 | window?.addEventListener("eip6963:requestProvider", () => announceProvider());
48 | announceProvider();
49 | }
50 |
51 | export { hereWalletProvider };
52 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import BN from "bn.js";
2 | import { Account, Connection } from "@near-js/accounts";
3 | import { FinalExecutionOutcome } from "@near-js/types";
4 | import { KeyPair, PublicKey } from "@near-js/crypto";
5 |
6 | import { Base64, Optional, Transaction } from "./helpers/types";
7 | import { HereAuthStorage } from "./storage/HereKeyStore";
8 | import { HereStrategy } from "./strategies/HereStrategy";
9 | import { InjectedState } from "./helpers/waitInjected";
10 |
11 | declare global {
12 | var Telegram: { WebApp?: any } | undefined;
13 | var hotExtension: any | undefined;
14 | }
15 |
16 | export type SelectorType = { id?: string; type?: string };
17 | export type HereProviderSign =
18 | | ({
19 | network?: string;
20 | type: "sign";
21 | selector?: SelectorType;
22 | callbackUrl?: string;
23 | telegramApp?: string;
24 | } & SignMessageOptionsLegacy)
25 | | {
26 | network?: string;
27 | type: "sign";
28 | nonce: number[];
29 | message: string;
30 | recipient: string;
31 | selector: SelectorType;
32 | callbackUrl?: string;
33 | telegramApp?: string;
34 | };
35 |
36 | export type HereProviderCall = {
37 | network?: string;
38 | transactions: HereCall[];
39 | type: "call";
40 | selector: SelectorType;
41 | callbackUrl?: string;
42 | telegramApp?: string;
43 | };
44 |
45 | export type HereProviderImport = {
46 | type: "import";
47 | keystore: string;
48 | network?: string;
49 | selector: SelectorType;
50 | callbackUrl?: string;
51 | telegramApp?: string;
52 | };
53 |
54 | export type HereProviderKeypom = {
55 | type: "keypom";
56 | contract: string;
57 | secret: string;
58 | selector: SelectorType;
59 | callbackUrl?: string;
60 | telegramApp?: string;
61 | };
62 |
63 | export type HereProviderRequest = HereProviderCall | HereProviderSign | HereProviderImport | HereProviderKeypom;
64 |
65 | export enum HereProviderStatus {
66 | APPROVING = 1,
67 | FAILED = 2,
68 | SUCCESS = 3,
69 | }
70 |
71 | export interface HereProviderResult {
72 | type: string;
73 | path?: string;
74 | public_key?: string;
75 | account_id?: string;
76 | payload?: string;
77 | topic?: string;
78 | status: HereProviderStatus;
79 | }
80 |
81 | export class HereProviderError extends Error {
82 | constructor(readonly payload?: string, readonly parentError?: Error) {
83 | super(payload ?? parentError?.message);
84 | }
85 | }
86 |
87 | export type HereCall = Optional;
88 |
89 | export interface HereAsyncOptions {
90 | signal?: AbortSignal;
91 | strategy?: HereStrategy;
92 | selector?: { type: string; id?: string };
93 | callbackUrl?: string;
94 | }
95 |
96 | export interface SignInOptions extends HereAsyncOptions {
97 | contractId?: string;
98 | allowance?: string;
99 | methodNames?: string[];
100 | }
101 |
102 | export type SignAndSendTransactionOptions = HereAsyncOptions & HereCall;
103 |
104 | export type SignMessageOptionsLegacy = {
105 | nonce?: number[];
106 | message: string;
107 | receiver: string;
108 | };
109 |
110 | export type SignMessageOptionsNEP0413 = {
111 | message: string; // The message that wants to be transmitted.
112 | recipient: string; // The recipient to whom the message is destined (e.g. "alice.near" or "myapp.com").
113 | nonce: Buffer; // A nonce that uniquely identifies this instance of the message, denoted as a 32 bytes array (a fixed `Buffer` in JS/TS).
114 | callbackUrl?: string; // Optional, applicable to browser wallets (e.g. MyNearWallet). The URL to call after the signing process. Defaults to `window.location.href`.
115 | };
116 |
117 | export type SignMessageOptions = (HereAsyncOptions & SignMessageOptionsNEP0413) | (HereAsyncOptions & SignMessageOptionsLegacy);
118 |
119 | export type SignMessageLegacyReturn = {
120 | signature: Uint8Array;
121 | publicKey: PublicKey;
122 | accountId: string;
123 | message: string;
124 | nonce: number[];
125 | receiver: string;
126 | };
127 |
128 | export type SignedMessageNEP0413 = {
129 | signature: Base64;
130 | publicKey: string;
131 | accountId: string;
132 | };
133 |
134 | export interface SignAndSendTransactionsOptions extends HereAsyncOptions {
135 | transactions: HereCall[];
136 | }
137 |
138 | export interface HereInitializeOptions {
139 | nodeUrl?: string;
140 | networkId?: "mainnet" | "testnet";
141 | authStorage?: HereAuthStorage;
142 | botId?: string;
143 | walletId?: string;
144 | defaultStrategy?: HereStrategy;
145 | injected?: InjectedState;
146 | }
147 |
148 | export interface HereStrategyRequest {
149 | id?: string;
150 | request: HereProviderRequest;
151 | disableCleanupRequest?: boolean;
152 | signal?: AbortSignal;
153 | accessKey?: KeyPair;
154 | callbackUrl?: string;
155 | }
156 |
157 | export interface HereWalletProtocol {
158 | readonly networkId: string;
159 | readonly connection: Connection;
160 | readonly authStorage: HereAuthStorage;
161 | readonly strategy: HereStrategy;
162 |
163 | account(id?: string): Promise;
164 | getAccounts(): Promise;
165 | switchAccount(id: string): Promise;
166 | getAccountId(): Promise;
167 | isSignedIn: () => Promise;
168 | signOut: () => Promise;
169 |
170 | getHereBalance: () => Promise;
171 | getAvailableBalance: () => Promise;
172 | signIn: (data: SignInOptions) => Promise;
173 | signAndSendTransaction: (data: SignAndSendTransactionOptions) => Promise;
174 | signAndSendTransactions: (data: SignAndSendTransactionsOptions) => Promise>;
175 | signMessage: {
176 | (data: HereAsyncOptions & SignMessageOptionsNEP0413): Promise;
177 | (data: HereAsyncOptions & SignMessageOptionsLegacy): Promise;
178 | };
179 | }
180 |
--------------------------------------------------------------------------------
/src/wallet.ts:
--------------------------------------------------------------------------------
1 | import { Account, Connection } from "@near-js/accounts";
2 | import { InMemorySigner } from "@near-js/signers";
3 | import { JsonRpcProvider } from "@near-js/providers";
4 | import { FinalExecutionOutcome } from "@near-js/types";
5 | import { PublicKey, KeyPair, KeyPairEd25519 } from "@near-js/crypto";
6 | import { randomBytes } from "crypto";
7 | import { sha256 } from "js-sha256";
8 | import BN from "bn.js";
9 |
10 | import { createAction } from "./helpers/actions";
11 | import { verifySignature } from "./helpers/nep0314";
12 | import { internalThrow, isValidAccessKey, serializeActions } from "./helpers/utils";
13 | import { HereAuthStorage, HereKeyStore } from "./storage/HereKeyStore";
14 | import { WidgetStrategy } from "./strategies/WidgetStrategy";
15 | import { HereStrategy } from "./strategies/HereStrategy";
16 |
17 | import {
18 | HereCall,
19 | HereAsyncOptions,
20 | HereWalletProtocol,
21 | SignAndSendTransactionOptions,
22 | SignAndSendTransactionsOptions,
23 | SignMessageOptions,
24 | SignInOptions,
25 | HereInitializeOptions,
26 | SignMessageOptionsNEP0413,
27 | SignMessageOptionsLegacy,
28 | SignMessageLegacyReturn,
29 | SignedMessageNEP0413,
30 | HereProviderStatus,
31 | } from "./types";
32 | import { TelegramAppStrategy } from "./strategies/TelegramAppStrategy";
33 | import { InjectedStrategy } from "./strategies/InjectedStrategy";
34 | import { waitInjectedHereWallet } from "./helpers/waitInjected";
35 | import { hereWalletProvider } from "./telegramEthereumProvider";
36 |
37 | class AccessDenied extends Error {}
38 |
39 | export class HereWallet implements HereWalletProtocol {
40 | readonly connection: Connection;
41 | readonly authStorage: HereAuthStorage;
42 | readonly strategy: HereStrategy;
43 |
44 | static async connect(options: HereInitializeOptions = {}) {
45 | if (options.authStorage == null) options.authStorage = new HereKeyStore();
46 |
47 | if (options.defaultStrategy) {
48 | const wallet = new HereWallet(options);
49 | await wallet.strategy.connect(wallet);
50 | return wallet;
51 | }
52 |
53 | if (typeof window !== "undefined") {
54 | if (window.hotExtension) {
55 | options.defaultStrategy = new InjectedStrategy();
56 | return new HereWallet(options);
57 | }
58 |
59 | if (window !== parent) {
60 | const injected = await waitInjectedHereWallet;
61 | if (injected != null) {
62 | options.defaultStrategy = new InjectedStrategy();
63 | const wallet = new HereWallet({ ...options, injected });
64 | await wallet.strategy.connect(wallet);
65 | return wallet;
66 | }
67 | }
68 |
69 | if (window.Telegram?.WebApp != null) {
70 | options.defaultStrategy = new TelegramAppStrategy(options.botId, options.walletId);
71 | const wallet = new HereWallet(options);
72 | await wallet.strategy.connect(wallet);
73 | return wallet;
74 | }
75 | }
76 |
77 | options.defaultStrategy = new WidgetStrategy();
78 | const wallet = new HereWallet(options);
79 | await wallet.strategy.connect(wallet);
80 | return wallet;
81 | }
82 |
83 | readonly ethProvider?: any;
84 | readonly ethAddress?: string;
85 | readonly telegramId?: number;
86 |
87 | private constructor({ injected, nodeUrl, networkId = "mainnet", authStorage, defaultStrategy }: HereInitializeOptions = {}) {
88 | this.authStorage = authStorage!;
89 | this.strategy = defaultStrategy!;
90 |
91 | Object.defineProperty(this, "ethAddress", { get: () => injected?.ethAddress });
92 | Object.defineProperty(this, "telegramId", { get: () => injected?.telegramId });
93 | Object.defineProperty(this, "ethProvider", { get: () => (injected?.ethAddress ? hereWalletProvider : null) });
94 |
95 | const signer = new InMemorySigner(this.authStorage);
96 | const rpc = new JsonRpcProvider({ url: nodeUrl ?? `https://rpc.${networkId}.near.org` });
97 | this.connection = Connection.fromConfig({
98 | jsvmAccountId: `jsvm.${networkId}`,
99 | provider: rpc,
100 | networkId,
101 | signer,
102 | });
103 | }
104 |
105 | get rpc() {
106 | return this.connection.provider;
107 | }
108 |
109 | get signer() {
110 | return this.connection.signer;
111 | }
112 |
113 | get networkId() {
114 | return this.connection.networkId;
115 | }
116 |
117 | public async account(id?: string) {
118 | const accountId = id ?? (await this.authStorage.getActiveAccount(this.networkId));
119 | if (accountId == null) throw new AccessDenied("Wallet not signed in");
120 | return new Account(this.connection, accountId);
121 | }
122 |
123 | public async isSignedIn() {
124 | const id = await this.authStorage.getActiveAccount(this.networkId);
125 | return id != null;
126 | }
127 |
128 | public async signOut() {
129 | const accountId = await this.authStorage.getActiveAccount(this.networkId);
130 | if (accountId == null) throw new Error("Wallet not signed in");
131 |
132 | const key = await this.authStorage.getKey(this.networkId, accountId);
133 | if (key != null) {
134 | const publicKey = key.getPublicKey().toString();
135 | await this.silentSignAndSendTransaction({
136 | receiverId: accountId,
137 | actions: [{ type: "DeleteKey", params: { publicKey } }],
138 | }).catch(() => {});
139 | }
140 |
141 | await this.authStorage.removeKey(this.networkId, accountId);
142 | }
143 |
144 | public async getHereBalance(id?: string) {
145 | const account = await this.account(id);
146 | const contractId = this.networkId === "mainnet" ? "here.storage.near" : "here.storage.testnet";
147 | const hereCoins = await account
148 | .viewFunction({ args: { account_id: account.accountId }, methodName: "ft_balance_of", contractId })
149 | .catch(() => "0");
150 |
151 | return new BN(hereCoins);
152 | }
153 |
154 | public async getAvailableBalance(id?: string): Promise {
155 | const account = await this.account(id);
156 | const result = await account.getAccountBalance();
157 | const hereBalance = await this.getHereBalance();
158 | return new BN(result.available).add(new BN(hereBalance));
159 | }
160 |
161 | public async getAccounts() {
162 | return await this.authStorage.getAccounts(this.networkId);
163 | }
164 |
165 | public async getAccountId() {
166 | const accountId = await this.authStorage.getActiveAccount(this.networkId);
167 | if (accountId == null) throw new Error("Wallet not signed in");
168 | return accountId;
169 | }
170 |
171 | public async switchAccount(id: string) {
172 | const key = await this.authStorage.getKey(this.networkId, id);
173 | if (key == null) throw new Error(`Account ${id} not signed in`);
174 | await this.authStorage.setActiveAccount(this.networkId, id);
175 | }
176 |
177 | public async signIn({
178 | contractId,
179 | allowance,
180 | methodNames = [],
181 | strategy = this.strategy,
182 | signal,
183 | callbackUrl,
184 | selector,
185 | }: SignInOptions = {}): Promise {
186 | if (contractId == null) {
187 | const { accountId } = await this.authenticate({ strategy, signal, selector });
188 |
189 | // Generate random keypair
190 | await this.authStorage.setKey(this.networkId, accountId, KeyPairEd25519.fromRandom());
191 | await this.authStorage.setActiveAccount(this.networkId, accountId);
192 | return accountId;
193 | }
194 |
195 | await strategy.onInitialized();
196 |
197 | try {
198 | const accessKey = KeyPair.fromRandom("ed25519");
199 | const permission = { receiverId: contractId, methodNames, allowance };
200 | const data = await strategy.request({
201 | signal,
202 | accessKey,
203 | callbackUrl,
204 | request: {
205 | type: "call",
206 | network: this.networkId,
207 | selector: selector || {},
208 | transactions: [
209 | {
210 | actions: [
211 | {
212 | type: "AddKey",
213 | params: {
214 | publicKey: accessKey.getPublicKey().toString(),
215 | accessKey: { permission },
216 | },
217 | },
218 | ],
219 | },
220 | ],
221 | },
222 | });
223 |
224 | if (data.account_id == null) {
225 | throw Error("Transaction is failed");
226 | }
227 |
228 | await this.authStorage.setKey(this.networkId, data.account_id, accessKey);
229 | await this.authStorage.setActiveAccount(this.networkId, data.account_id);
230 | return data.account_id;
231 | } catch (error) {
232 | internalThrow(error, strategy, selector);
233 | throw error;
234 | }
235 | }
236 |
237 | public async silentSignAndSendTransaction({ actions, receiverId, signerId }: HereCall) {
238 | const account = await this.account(signerId);
239 | const localKey = await this.authStorage.getKey(this.networkId, account.accountId).catch(() => null);
240 | if (localKey == null) throw new AccessDenied();
241 |
242 | const publicKey = localKey.getPublicKey();
243 | const accessKeys = await account.getAccessKeys();
244 |
245 | const call = { receiverId, actions };
246 | const isValid = accessKeys.some((v) => {
247 | if (v.public_key !== publicKey.toString()) return false;
248 | return isValidAccessKey(account.accountId, v, call);
249 | });
250 |
251 | if (isValid === false) throw new AccessDenied();
252 |
253 | return await account.signAndSendTransaction({
254 | actions: actions.map((a) => createAction(a)),
255 | receiverId: receiverId ?? account.accountId,
256 | });
257 | }
258 |
259 | public async signAndSendTransaction(opts: SignAndSendTransactionOptions) {
260 | const { signerId, receiverId, actions, callbackUrl, strategy = this.strategy, signal, selector } = opts;
261 | await strategy.onInitialized();
262 |
263 | try {
264 | const result = await this.silentSignAndSendTransaction({ receiverId, actions, signerId });
265 | const success = { type: "web", status: HereProviderStatus.SUCCESS, payload: result?.transaction_outcome.id };
266 | strategy.onSuccess(success);
267 | return result;
268 | } catch (e: any) {
269 | try {
270 | // If silent sign return AccessDenied or NotEnoughAllowance we request mobile wallet
271 | // OR its just transaction error
272 | if (!(e instanceof AccessDenied) && e?.type !== "NotEnoughAllowance") {
273 | internalThrow(e, strategy, selector);
274 | throw e;
275 | }
276 |
277 | const activeAccount = await this.getAccountId().catch(() => undefined);
278 | const data = await strategy.request({
279 | signal,
280 | callbackUrl,
281 | request: {
282 | type: "call",
283 | network: this.networkId,
284 | transactions: [{ actions: serializeActions(actions), receiverId, signerId }],
285 | selector: opts.selector || { id: signerId || activeAccount },
286 | },
287 | });
288 |
289 | if (data.payload == null || data.account_id == null) {
290 | throw Error("Transaction not found, but maybe executed");
291 | }
292 |
293 | return await this.rpc.txStatus(data.payload, data.account_id, "INCLUDED");
294 | } catch (error) {
295 | internalThrow(error, strategy, selector);
296 | throw error;
297 | }
298 | }
299 | }
300 |
301 | async verifyMessageNEP0413(request: SignMessageOptionsNEP0413, result: SignedMessageNEP0413) {
302 | const isSignatureValid = verifySignature(request, result);
303 | if (!isSignatureValid) throw Error("Incorrect signature");
304 |
305 | const account = await this.account(result.accountId);
306 | const keys = await account.getAccessKeys();
307 | const isFullAccess = keys.some((k) => {
308 | if (k.public_key !== result.publicKey) return false;
309 | if (k.access_key.permission !== "FullAccess") return false;
310 | return true;
311 | });
312 |
313 | if (!isFullAccess) throw Error("Signer public key is not full access");
314 | return true;
315 | }
316 |
317 | async authenticate(options: HereAsyncOptions & Partial = {}) {
318 | const signRequest = {
319 | nonce: options.nonce ?? randomBytes(32),
320 | recipient: options.recipient ?? window?.location.host,
321 | message: options.message ?? "Authenticate",
322 | };
323 |
324 | const signed = await this.signMessage({ ...signRequest, ...options });
325 | await this.verifyMessageNEP0413(signRequest, signed);
326 | return signed;
327 | }
328 |
329 | public signMessage(options: HereAsyncOptions & SignMessageOptionsNEP0413): Promise;
330 | public signMessage(options: HereAsyncOptions & SignMessageOptionsLegacy): Promise;
331 | public async signMessage(options: SignMessageOptions): Promise {
332 | const { strategy = this.strategy, signal, selector, callbackUrl } = options;
333 | await strategy.onInitialized();
334 |
335 | // Legacy format with receiver property, does not correspond to the current version of the standard
336 | if ("receiver" in options) return await this.legacySignMessage(options);
337 |
338 | const activeAccount = await this.getAccountId().catch(() => undefined);
339 | const data = await strategy.request({
340 | signal,
341 | callbackUrl,
342 | request: {
343 | type: "sign",
344 | message: options.message,
345 | recipient: options.recipient,
346 | nonce: Array.from(options.nonce),
347 | network: this.networkId,
348 | selector: selector || { id: activeAccount },
349 | },
350 | });
351 |
352 | if (data?.payload == null) throw Error("Signature not found");
353 | const { publicKey, signature, accountId }: SignedMessageNEP0413 = JSON.parse(data.payload);
354 | return { publicKey, signature, accountId };
355 | }
356 |
357 | async legacySignMessage({ receiver, message, nonce, ...delegate }: SignMessageOptionsLegacy & HereAsyncOptions): Promise {
358 | if (nonce == null) {
359 | let nonceArray: Uint8Array = new Uint8Array(32);
360 | nonce = [...crypto.getRandomValues(nonceArray)];
361 | }
362 |
363 | const { strategy = this.strategy, callbackUrl, selector, signal } = delegate;
364 | const activeAccount = await this.getAccountId().catch(() => undefined);
365 | const data = await strategy.request({
366 | signal,
367 | callbackUrl,
368 | request: {
369 | type: "sign",
370 | network: this.networkId,
371 | selector: selector || { id: activeAccount },
372 | message,
373 | receiver,
374 | nonce,
375 | },
376 | });
377 |
378 | if (data?.payload == null) {
379 | throw Error("Signature not found");
380 | }
381 |
382 | try {
383 | const { publicKey, signature, accountId }: SignedMessageNEP0413 = JSON.parse(data.payload);
384 | const sign = new Uint8Array(Buffer.from(signature, "base64"));
385 | const json = JSON.stringify({ message, receiver, nonce });
386 | const msg = new Uint8Array(sha256.digest(`NEP0413:` + json));
387 | const isVerify = PublicKey.from(publicKey).verify(msg, sign);
388 | if (isVerify === false) throw Error();
389 |
390 | const account = await this.account(accountId);
391 | const keys = await account.getAccessKeys();
392 | const pb = publicKey.toString();
393 | const isValid = keys.some((k) => {
394 | if (k.public_key !== pb) return false;
395 | if (k.access_key.permission !== "FullAccess") return false;
396 | return true;
397 | });
398 |
399 | if (isValid === false) throw Error();
400 | return {
401 | signature: new Uint8Array(Buffer.from(signature, "base64")),
402 | publicKey: PublicKey.from(publicKey),
403 | message: `NEP0413:` + json,
404 | receiver,
405 | accountId,
406 | nonce,
407 | };
408 | } catch {
409 | throw Error("Signature not correct");
410 | }
411 | }
412 |
413 | public async signAndSendTransactions({ transactions, ...delegate }: SignAndSendTransactionsOptions) {
414 | const { strategy = this.strategy, selector, callbackUrl, signal } = delegate;
415 | await strategy.onInitialized();
416 |
417 | let results: FinalExecutionOutcome[] = [];
418 | try {
419 | for (const call of transactions) {
420 | const r = await this.silentSignAndSendTransaction(call);
421 | results.push(r);
422 | }
423 |
424 | const payload = results.map((result) => result.transaction_outcome.id).join(",");
425 | const success = { type: "web", status: HereProviderStatus.SUCCESS, payload };
426 | strategy.onSuccess(success);
427 | return results;
428 | } catch (e: any) {
429 | try {
430 | // If silent sign return access denied or not enough balance we request mobile wallet
431 | // OR its just transaction error
432 | if (!(e instanceof AccessDenied) && e?.type !== "NotEnoughAllowance") {
433 | internalThrow(e, strategy, selector);
434 | throw e;
435 | }
436 |
437 | const activeAccount = await this.getAccountId().catch(() => undefined);
438 | const uncompleted = transactions.slice(results.length);
439 | const data = await strategy.request({
440 | signal,
441 | callbackUrl,
442 | request: {
443 | type: "call",
444 | network: this.networkId,
445 | selector: selector || { id: uncompleted[0].signerId || activeAccount },
446 | transactions: uncompleted.map((trx) => ({ ...trx, actions: serializeActions(trx.actions) })),
447 | },
448 | });
449 |
450 | if (data.payload == null || data.account_id == null) {
451 | throw Error("Transaction not found, but maybe executed");
452 | }
453 |
454 | const promises = data.payload.split(",").map((id) => this.rpc.txStatus(id, data.account_id!, "INCLUDED"));
455 | return await Promise.all(promises);
456 | } catch (error) {
457 | internalThrow(error, strategy, selector);
458 | throw error;
459 | }
460 | }
461 | }
462 | }
463 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "moduleResolution": "node",
5 | "alwaysStrict": true,
6 | "allowJs": true,
7 | "strict": true,
8 | "rootDir": "./src",
9 | "outDir": "./build",
10 | "declaration": true,
11 | "sourceMap": true,
12 | "emitDecoratorMetadata": true,
13 | "experimentalDecorators": true,
14 | "allowSyntheticDefaultImports": true,
15 | "target": "es2015",
16 | "module": "CommonJS",
17 | "lib": ["ESNext", "dom"]
18 | },
19 | "exclude": ["node_modules", "build", "dist", "example"]
20 | }
21 |
--------------------------------------------------------------------------------