@mainnet-cash/postgresql-storage 2.1.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +3 -0
  2. package/dist/module/SqlProvider.d.ts +30 -0
  3. package/dist/module/SqlProvider.js +159 -0
  4. package/dist/module/SqlProvider.js.map +1 -0
  5. package/dist/module/index.d.ts +2 -0
  6. package/dist/module/index.js +3 -0
  7. package/dist/module/index.js.map +1 -0
  8. package/dist/module/util.d.ts +7 -0
  9. package/dist/module/util.js +24 -0
  10. package/dist/module/util.js.map +1 -0
  11. package/dist/module/webhook/Webhook.d.ts +35 -0
  12. package/dist/module/webhook/Webhook.js +77 -0
  13. package/dist/module/webhook/Webhook.js.map +1 -0
  14. package/dist/module/webhook/WebhookBch.d.ts +13 -0
  15. package/dist/module/webhook/WebhookBch.js +141 -0
  16. package/dist/module/webhook/WebhookBch.js.map +1 -0
  17. package/dist/module/webhook/WebhookWorker.d.ts +22 -0
  18. package/dist/module/webhook/WebhookWorker.js +94 -0
  19. package/dist/module/webhook/WebhookWorker.js.map +1 -0
  20. package/dist/module/webhook/index.d.ts +4 -0
  21. package/dist/module/webhook/index.js +5 -0
  22. package/dist/module/webhook/index.js.map +1 -0
  23. package/dist/module/webhook/interface.d.ts +7 -0
  24. package/dist/module/webhook/interface.js +2 -0
  25. package/dist/module/webhook/interface.js.map +1 -0
  26. package/dist/tsconfig.browser.tsbuildinfo +1 -0
  27. package/package.json +34 -0
  28. package/src/SqlProvider.test.ts +264 -0
  29. package/src/SqlProvider.ts +233 -0
  30. package/src/Wallet.test.ts +571 -0
  31. package/src/createWallet.test.ts +158 -0
  32. package/src/index.test.ts +67 -0
  33. package/src/index.ts +2 -0
  34. package/src/util.ts +30 -0
  35. package/src/webhook/Webhook.test.ts +9 -0
  36. package/src/webhook/Webhook.ts +99 -0
  37. package/src/webhook/WebhookBch.test.ts +323 -0
  38. package/src/webhook/WebhookBch.ts +198 -0
  39. package/src/webhook/WebhookWorker.test.ts +94 -0
  40. package/src/webhook/WebhookWorker.ts +119 -0
  41. package/src/webhook/index.ts +4 -0
  42. package/src/webhook/interface.ts +7 -0
  43. package/tsconfig.browser.json +6 -0
  44. package/tsconfig.json +28 -0
@@ -0,0 +1,198 @@
1
+ import SqlProvider from "../SqlProvider.js";
2
+ import { TxI } from "mainnet-js";
3
+ import { ElectrumRawTransaction } from "mainnet-js";
4
+ import { balanceResponseFromSatoshi } from "mainnet-js";
5
+ import { Wallet } from "mainnet-js";
6
+ import { Webhook, WebhookRecurrence, WebhookType } from "./Webhook.js";
7
+ import WebhookWorker from "./WebhookWorker.js";
8
+
9
+ export class WebhookBch extends Webhook {
10
+ callback!: (data: any | string | Array<string>) => void;
11
+ wallet!: Wallet;
12
+
13
+ db!: SqlProvider;
14
+ seenStatuses: string[] = [];
15
+
16
+ constructor(hook: Webhook | Object) {
17
+ super(hook);
18
+ Object.assign(this, hook);
19
+ }
20
+
21
+ async stop(): Promise<void> {
22
+ await this.wallet.provider!.unsubscribeFromAddress(
23
+ this.cashaddr,
24
+ this.callback
25
+ );
26
+ }
27
+
28
+ async start(): Promise<void> {
29
+ const webhookCallback = async (data: string | Array<string>) => {
30
+ let status: string = "";
31
+ if (typeof data === "string") {
32
+ // subscription acknowledgement notification with current status
33
+ status = data;
34
+
35
+ // we should skip fetching transactions for freshly registered hooks upon acknowledements
36
+ if (this.status === "") {
37
+ return;
38
+ }
39
+ } else if (data instanceof Array) {
40
+ status = data[1];
41
+ if (data[0] !== this.cashaddr) {
42
+ // console.warn("Address missmatch, skipping", data[0], this.cashaddr);
43
+ return;
44
+ }
45
+ } else {
46
+ return;
47
+ }
48
+
49
+ if (status != this.status && this.seenStatuses.indexOf(status) === -1) {
50
+ this.seenStatuses.push(status);
51
+ await this.handler(status);
52
+ }
53
+ };
54
+
55
+ this.callback = webhookCallback;
56
+ this.wallet = await Wallet.fromCashaddr(this.cashaddr);
57
+ await this.wallet.provider!.subscribeToAddress(
58
+ this.cashaddr,
59
+ this.callback
60
+ );
61
+ }
62
+
63
+ async handler(status: string): Promise<void> {
64
+ // console.debug("Dispatching action for a webhook", this);
65
+ // get transactions
66
+ const history: TxI[] = await this.wallet.provider!.getHistory(
67
+ this.cashaddr
68
+ );
69
+
70
+ // figure out which transactions to send to the hook
71
+ let txs: TxI[] = [];
72
+
73
+ if (status === "") {
74
+ // send the last transaction only if no tracking was done
75
+ txs = history.slice(-1);
76
+ } else {
77
+ // reverse history for faster search
78
+ const revHistory = history.reverse();
79
+ // update height of transactions, which are now confirmed
80
+ this.tx_seen = this.tx_seen.map((seenTx) => {
81
+ if (seenTx.height <= 0) {
82
+ const histTx = revHistory.find(
83
+ (val) => val.tx_hash === seenTx.tx_hash
84
+ );
85
+ if (histTx) {
86
+ seenTx.height = histTx.height;
87
+ }
88
+ }
89
+ return seenTx;
90
+ });
91
+
92
+ const seenHashes = this.tx_seen.map((val) => val.tx_hash);
93
+ txs = history.filter(
94
+ (val) =>
95
+ (val.height >= this.last_height || val.height <= 0) &&
96
+ !seenHashes.includes(val.tx_hash)
97
+ );
98
+ }
99
+
100
+ let shouldUpdateStatus: boolean = true;
101
+
102
+ for (const tx of txs) {
103
+ // watching transactions
104
+ let result: boolean = false;
105
+
106
+ if (this.type.indexOf("transaction:") >= 0) {
107
+ // console.debug("Getting raw tx", tx.tx_hash);
108
+ const rawTx: ElectrumRawTransaction =
109
+ await this.wallet.provider!.getRawTransactionObject(tx.tx_hash);
110
+ const parentTxs: ElectrumRawTransaction[] = await Promise.all(
111
+ rawTx.vin.map((t) =>
112
+ this.wallet.provider!.getRawTransactionObject(t.txid)
113
+ )
114
+ );
115
+ // console.debug("Got raw tx", JSON.stringify(rawTx, null, 2));
116
+ const haveAddressInOutputs: boolean = rawTx.vout.some((val) =>
117
+ val.scriptPubKey.addresses.includes(this.cashaddr)
118
+ );
119
+ const haveAddressInParentOutputs: boolean = parentTxs.some((parent) =>
120
+ parent.vout.some((val) =>
121
+ val.scriptPubKey.addresses.includes(this.cashaddr)
122
+ )
123
+ );
124
+
125
+ const wantsIn: boolean = this.type.indexOf("in") >= 0;
126
+ const wantsOut: boolean = this.type.indexOf("out") >= 0;
127
+
128
+ const txDirection: string =
129
+ haveAddressInParentOutputs && haveAddressInOutputs
130
+ ? WebhookType.transactionInOut
131
+ : haveAddressInParentOutputs
132
+ ? WebhookType.transactionOut
133
+ : WebhookType.transactionIn;
134
+
135
+ if (wantsIn && haveAddressInOutputs) {
136
+ result = await this.post({
137
+ direction: txDirection,
138
+ data: rawTx,
139
+ });
140
+ } else if (wantsOut && haveAddressInParentOutputs) {
141
+ result = await this.post({
142
+ direction: txDirection,
143
+ data: rawTx,
144
+ });
145
+ } else {
146
+ // not interested in this transaction
147
+ continue;
148
+ }
149
+ } else if (this.type === WebhookType.balance) {
150
+ // watching address balance
151
+ const balanceSat = await this.wallet.provider!.getBalance(
152
+ this.cashaddr
153
+ );
154
+ const balanceObject = await balanceResponseFromSatoshi(balanceSat);
155
+ result = await this.post(balanceObject);
156
+ }
157
+
158
+ if (result) {
159
+ this.tx_seen.push(tx);
160
+ await this.db.setWebhookSeenTxLastHeight(
161
+ this.id!,
162
+ this.tx_seen,
163
+ this.last_height
164
+ );
165
+ } else {
166
+ // console.debug("Failed to execute webhook", hook);
167
+ shouldUpdateStatus = false;
168
+ }
169
+ }
170
+
171
+ // successful run
172
+ if (shouldUpdateStatus) {
173
+ if (this.recurrence === WebhookRecurrence.once) {
174
+ // we have to notify the worker about end of life
175
+ await (await WebhookWorker.instance()).stopHook(this);
176
+ await this.destroy();
177
+ return;
178
+ }
179
+
180
+ this.status = status;
181
+ await this.db.setWebhookStatus(this.id!, status);
182
+
183
+ // update last_height and truncate tx_seen
184
+ const maxHeight = Math.max(...this.tx_seen.map((val) => val.height));
185
+ if (maxHeight >= this.last_height) {
186
+ this.last_height = maxHeight;
187
+ this.tx_seen = this.tx_seen.filter(
188
+ (val) => val.height === maxHeight || val.height <= 0
189
+ );
190
+ await this.db.setWebhookSeenTxLastHeight(
191
+ this.id!,
192
+ this.tx_seen,
193
+ this.last_height
194
+ );
195
+ }
196
+ }
197
+ }
198
+ }
@@ -0,0 +1,94 @@
1
+ import WebhookWorker from "../webhook/WebhookWorker";
2
+ import { Webhook } from "./Webhook";
3
+
4
+ let worker: WebhookWorker;
5
+ let alice = "";
6
+ let aliceWif = "";
7
+
8
+ /**
9
+ * @jest-environment jsdom
10
+ */
11
+
12
+ describe("Webhook worker tests", () => {
13
+ beforeAll(async () => {
14
+ try {
15
+ if (process.env.PRIVATE_WIF) {
16
+ alice = process.env.ADDRESS!;
17
+ aliceWif = `wif:regtest:${process.env.PRIVATE_WIF!}`;
18
+ } else {
19
+ console.error("regtest env vars not set");
20
+ }
21
+
22
+ Webhook.debug.setupAxiosMocks();
23
+ worker = await WebhookWorker.instance();
24
+ } catch (e: any) {
25
+ throw e;
26
+ }
27
+ });
28
+
29
+ beforeEach(async () => {
30
+ worker.deleteAllWebhooks();
31
+ });
32
+
33
+ afterEach(async () => {
34
+ Webhook.debug.reset();
35
+ });
36
+
37
+ afterAll(async () => {
38
+ await worker.destroy();
39
+ await worker.db.close();
40
+ });
41
+
42
+ test("Test posting hook", async () => {
43
+ const hook1 = new Webhook({ url: "http://example.com/pass" });
44
+ let success = await hook1.post({});
45
+ expect(success).toBe(true);
46
+
47
+ const hook2 = new Webhook({ url: "http://example.com/fail" });
48
+ let fail = await hook2.post({});
49
+ expect(fail).toBe(false);
50
+
51
+ expect(Webhook.debug.responses["http://example.com/pass"].length).toBe(1);
52
+
53
+ expect(Webhook.debug.responses["http://example.com/fail"].length).toBe(1);
54
+ });
55
+
56
+ test("Test empty hook db", async () => {
57
+ try {
58
+ await new Promise((resolve) =>
59
+ setTimeout(async () => {
60
+ expect(worker.activeHooks.size).toBe(0);
61
+ expect(Webhook.debug.responses).toStrictEqual({});
62
+ resolve(true);
63
+ }, 0)
64
+ );
65
+ } catch (e: any) {
66
+ console.log(e, e.stack, e.message);
67
+ throw e;
68
+ }
69
+ });
70
+
71
+ test("Test starting with expired hook", async () => {
72
+ await worker.registerWebhook(
73
+ {
74
+ cashaddr: alice,
75
+ url: "http://example.com/pass",
76
+ type: "transaction:in",
77
+ recurrence: "once",
78
+ duration_sec: -1000,
79
+ },
80
+ false
81
+ );
82
+
83
+ await worker.init();
84
+
85
+ try {
86
+ expect(worker.activeHooks.size).toBe(0);
87
+ expect((await worker.db.getWebhooks()).length).toBe(0);
88
+ expect(Webhook.debug.responses).toStrictEqual({});
89
+ } catch (e: any) {
90
+ console.log(e, e.stack, e.message);
91
+ throw e;
92
+ }
93
+ });
94
+ });
@@ -0,0 +1,119 @@
1
+ import SqlProvider from "../SqlProvider.js";
2
+ import { RegisterWebhookParams } from "./interface.js";
3
+
4
+ import { Webhook } from "./Webhook";
5
+
6
+ export default class WebhookWorker {
7
+ activeHooks: Map<number, Webhook> = new Map();
8
+ callbacks: Map<number, (data: any | string | Array<string>) => void> =
9
+ new Map();
10
+ db: SqlProvider;
11
+ checkInterval: any = undefined;
12
+
13
+ private static _instance: WebhookWorker;
14
+
15
+ static async instance() {
16
+ if (!WebhookWorker._instance) {
17
+ WebhookWorker._instance = new WebhookWorker();
18
+ await WebhookWorker._instance.init();
19
+ }
20
+
21
+ return WebhookWorker._instance;
22
+ }
23
+
24
+ constructor() {
25
+ this.db = new SqlProvider();
26
+ }
27
+
28
+ async init(): Promise<void> {
29
+ await this.db.init();
30
+
31
+ await this.evictOldHooks();
32
+ await this.pickupHooks(true);
33
+ if (!this.checkInterval) {
34
+ this.checkInterval = setInterval(async () => {
35
+ await this.evictOldHooks();
36
+ await this.pickupHooks(true);
37
+ }, 5 * 60 * 1000);
38
+ }
39
+ }
40
+
41
+ async destroy(): Promise<void> {
42
+ await this.stopAll();
43
+ if (this.checkInterval) {
44
+ clearInterval(this.checkInterval);
45
+ this.checkInterval = undefined;
46
+ }
47
+ }
48
+
49
+ async pickupHooks(start: boolean = false): Promise<void> {
50
+ const hooks: Webhook[] = await this.db.getWebhooks();
51
+ for (const hook of hooks) {
52
+ if (!this.activeHooks.has(hook.id!)) {
53
+ this.activeHooks.set(hook.id!, hook);
54
+ if (start) {
55
+ await hook.start();
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ async evictOldHooks(): Promise<void> {
62
+ const now = new Date();
63
+ const dbHooks = await this.db.getWebhooks();
64
+ for (const hook of dbHooks) {
65
+ if (now >= hook.expires_at) {
66
+ // console.debug("Evicting expired hook with id", hook.id, new Date(), hook.expires_at);
67
+ if (this.activeHooks.has(hook.id!)) {
68
+ await this.stopHook(hook);
69
+ }
70
+ await this.db.deleteWebhook(hook.id!);
71
+ }
72
+ }
73
+ }
74
+
75
+ async registerWebhook(
76
+ params: RegisterWebhookParams,
77
+ start: boolean = true
78
+ ): Promise<number> {
79
+ const webhook = await this.db.addWebhook(params);
80
+ if (start) {
81
+ this.activeHooks.set(webhook.id!, webhook);
82
+ await webhook.start();
83
+ }
84
+ return webhook.id!;
85
+ }
86
+
87
+ async getWebhook(id: number): Promise<Webhook | undefined> {
88
+ if (this.activeHooks.has(id)) {
89
+ return this.activeHooks.get(id)!;
90
+ }
91
+
92
+ return await this.db.getWebhook(id);
93
+ }
94
+
95
+ async deleteWebhook(id: number): Promise<void> {
96
+ if (this.activeHooks.has(id)) {
97
+ await this.stopHook(this.activeHooks.get(id)!);
98
+ }
99
+ await this.db.deleteWebhook(id);
100
+ }
101
+
102
+ async deleteAllWebhooks(): Promise<void> {
103
+ await this.stopAll();
104
+ await this.db.clearWebhooks();
105
+ }
106
+
107
+ async stopAll(): Promise<void> {
108
+ for (const [, hook] of this.activeHooks) {
109
+ await this.stopHook(hook);
110
+ }
111
+ }
112
+
113
+ async stopHook(hook: Webhook): Promise<void> {
114
+ if (this.activeHooks.has(hook.id!)) {
115
+ await hook.stop();
116
+ this.activeHooks.delete(hook.id!);
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,4 @@
1
+ export { default as WebhookWorker } from "./WebhookWorker.js";
2
+ export * from "./Webhook.js";
3
+ export * from "./WebhookBch.js";
4
+ export * from "./interface.js";
@@ -0,0 +1,7 @@
1
+ export interface RegisterWebhookParams {
2
+ cashaddr: string;
3
+ url: string;
4
+ type: string;
5
+ recurrence: string;
6
+ duration_sec?: number;
7
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "./tsconfig",
3
+ "compilerOptions": {},
4
+ "include": ["src/**/*.ts"],
5
+ "exclude": ["node_modules/**", "dist/**", "src/**/*test.ts"]
6
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "skipLibCheck": true,
6
+ "esModuleInterop": true,
7
+ "allowSyntheticDefaultImports": true,
8
+ "strict": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "downlevelIteration": true,
11
+ "composite": true,
12
+ "module": "esnext",
13
+ "target": "esnext",
14
+ "outDir": "./dist/module",
15
+ "moduleResolution": "node",
16
+ "resolveJsonModule": true,
17
+ "lib": ["es2020", "es2020.bigint", "dom"],
18
+ "typeRoots": ["node_modules/@types", "./src/types"]
19
+ },
20
+ "include": ["src/**/*.ts"],
21
+ "exclude": ["node_modules/**", "src/**/*test.ts"],
22
+ "references": [
23
+ {
24
+ "path": "../mainnet-js/"
25
+ }
26
+ ],
27
+ "compileOnSave": false
28
+ }