Page MenuHomePhabricator

D15392.id45096.diff
No OneTemporary

D15392.id45096.diff

diff --git a/modules/chronik-client/.mocharc.js b/modules/chronik-client/.mocharc.js
--- a/modules/chronik-client/.mocharc.js
+++ b/modules/chronik-client/.mocharc.js
@@ -1,4 +1,4 @@
'use strict';
module.exports = {
- timeout: '10s',
+ timeout: '20s',
};
diff --git a/modules/chronik-client/README.md b/modules/chronik-client/README.md
--- a/modules/chronik-client/README.md
+++ b/modules/chronik-client/README.md
@@ -90,3 +90,4 @@
0.12.0 - Add support for `block(hashOrHeight)` and `blocks(startHeight, endHeight)` methods to `ChronikClientNode`
0.13.0 - Add support for `blockTxs(hashOrHeight, page, pageSize)` and `tx(txid)` methods to `ChronikClientNode`
0.14.0 - Add support for `rawTx(txid)` method to `ChronikClientNode`
+0.15.0 - Add support for `script` endpoints `history()` and `utxos()` to `ChronikClientNode`
diff --git a/modules/chronik-client/package-lock.json b/modules/chronik-client/package-lock.json
--- a/modules/chronik-client/package-lock.json
+++ b/modules/chronik-client/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "chronik-client",
- "version": "0.11.0",
+ "version": "0.15.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "chronik-client",
- "version": "0.11.0",
+ "version": "0.15.0",
"license": "MIT",
"dependencies": {
"@types/ws": "^8.2.1",
@@ -23,6 +23,7 @@
"@typescript-eslint/parser": "^5.5.0",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
+ "ecashaddrjs": "^1.5.6",
"eslint": "^8.37.0",
"eslint-config-prettier": "^8.3.0",
"mocha": "^9.1.3",
@@ -171,6 +172,18 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
+ "node_modules/@noble/hashes": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz",
+ "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -317,9 +330,9 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "20.11.5",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz",
- "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==",
+ "version": "20.11.16",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz",
+ "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==",
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -660,9 +673,9 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
- "version": "1.6.5",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
- "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
+ "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
"dependencies": {
"follow-redirects": "^1.15.4",
"form-data": "^4.0.0",
@@ -675,6 +688,21 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
+ "node_modules/base-x": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz",
+ "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==",
+ "dev": true
+ },
+ "node_modules/big-integer": {
+ "version": "1.6.36",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.36.tgz",
+ "integrity": "sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -712,6 +740,25 @@
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
"dev": true
},
+ "node_modules/bs58": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
+ "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
+ "dev": true,
+ "dependencies": {
+ "base-x": "^4.0.0"
+ }
+ },
+ "node_modules/bs58check": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz",
+ "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==",
+ "dev": true,
+ "dependencies": {
+ "@noble/hashes": "^1.2.0",
+ "bs58": "^5.0.0"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -1035,6 +1082,16 @@
"detect-libc": "^1.0.3"
}
},
+ "node_modules/ecashaddrjs": {
+ "version": "1.5.6",
+ "resolved": "https://registry.npmjs.org/ecashaddrjs/-/ecashaddrjs-1.5.6.tgz",
+ "integrity": "sha512-U3vF3pMkMTlgfwYEzWV2+MIItlTqHbOebRmV553DuKPjj6IFNWhqcOnAk8eyPGUJ83xG7VRXS6mv4BGZGMB0Kg==",
+ "dev": true,
+ "dependencies": {
+ "big-integer": "1.6.36",
+ "bs58check": "^3.0.1"
+ }
+ },
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -1042,9 +1099,9 @@
"dev": true
},
"node_modules/escalade": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
- "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
+ "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"dev": true,
"engines": {
"node": ">=6"
@@ -1305,9 +1362,9 @@
"dev": true
},
"node_modules/fastq": {
- "version": "1.16.0",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz",
- "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==",
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+ "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
"dev": true,
"dependencies": {
"reusify": "^1.0.4"
@@ -1559,9 +1616,9 @@
}
},
"node_modules/ignore": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
- "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
+ "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
"dev": true,
"engines": {
"node": ">= 4"
@@ -1750,9 +1807,9 @@
"dev": true
},
"node_modules/jsonc-parser": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
- "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz",
+ "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==",
"dev": true
},
"node_modules/keyv": {
@@ -2429,9 +2486,9 @@
]
},
"node_modules/semver": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+ "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -2623,9 +2680,9 @@
}
},
"node_modules/ts-proto": {
- "version": "1.166.3",
- "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.166.3.tgz",
- "integrity": "sha512-Ad3TV7wH7m62fKHI55faq403OW9hC5XOGev+bDXHB5lmx1AhiZiH1ISQdk1sk33fRRy8lEBrKRknV41F3d7Nfw==",
+ "version": "1.167.3",
+ "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.167.3.tgz",
+ "integrity": "sha512-quBKTeIgqhTGKXl5JN7HHZjLwrdMaNIoQ0+X11PqNLVRqTlnEzfiCUXM6HOW4pZa6PD5+qSyKOEGaTa3kS7Glg==",
"dev": true,
"dependencies": {
"case-anything": "^2.1.13",
@@ -3116,6 +3173,12 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
+ "@noble/hashes": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz",
+ "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==",
+ "dev": true
+ },
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3253,9 +3316,9 @@
"dev": true
},
"@types/node": {
- "version": "20.11.5",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz",
- "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==",
+ "version": "20.11.16",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz",
+ "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==",
"requires": {
"undici-types": "~5.26.4"
}
@@ -3471,9 +3534,9 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
- "version": "1.6.5",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
- "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
+ "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
"requires": {
"follow-redirects": "^1.15.4",
"form-data": "^4.0.0",
@@ -3486,6 +3549,18 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
+ "base-x": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz",
+ "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==",
+ "dev": true
+ },
+ "big-integer": {
+ "version": "1.6.36",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.36.tgz",
+ "integrity": "sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg==",
+ "dev": true
+ },
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -3517,6 +3592,25 @@
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
"dev": true
},
+ "bs58": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
+ "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
+ "dev": true,
+ "requires": {
+ "base-x": "^4.0.0"
+ }
+ },
+ "bs58check": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz",
+ "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==",
+ "dev": true,
+ "requires": {
+ "@noble/hashes": "^1.2.0",
+ "bs58": "^5.0.0"
+ }
+ },
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -3748,6 +3842,16 @@
"detect-libc": "^1.0.3"
}
},
+ "ecashaddrjs": {
+ "version": "1.5.6",
+ "resolved": "https://registry.npmjs.org/ecashaddrjs/-/ecashaddrjs-1.5.6.tgz",
+ "integrity": "sha512-U3vF3pMkMTlgfwYEzWV2+MIItlTqHbOebRmV553DuKPjj6IFNWhqcOnAk8eyPGUJ83xG7VRXS6mv4BGZGMB0Kg==",
+ "dev": true,
+ "requires": {
+ "big-integer": "1.6.36",
+ "bs58check": "^3.0.1"
+ }
+ },
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -3755,9 +3859,9 @@
"dev": true
},
"escalade": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
- "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
+ "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"dev": true
},
"escape-string-regexp": {
@@ -3955,9 +4059,9 @@
"dev": true
},
"fastq": {
- "version": "1.16.0",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz",
- "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==",
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+ "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
"dev": true,
"requires": {
"reusify": "^1.0.4"
@@ -4131,9 +4235,9 @@
"dev": true
},
"ignore": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
- "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
+ "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
"dev": true
},
"import-fresh": {
@@ -4274,9 +4378,9 @@
"dev": true
},
"jsonc-parser": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
- "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz",
+ "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==",
"dev": true
},
"keyv": {
@@ -4746,9 +4850,9 @@
"dev": true
},
"semver": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+ "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
@@ -4884,9 +4988,9 @@
}
},
"ts-proto": {
- "version": "1.166.3",
- "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.166.3.tgz",
- "integrity": "sha512-Ad3TV7wH7m62fKHI55faq403OW9hC5XOGev+bDXHB5lmx1AhiZiH1ISQdk1sk33fRRy8lEBrKRknV41F3d7Nfw==",
+ "version": "1.167.3",
+ "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.167.3.tgz",
+ "integrity": "sha512-quBKTeIgqhTGKXl5JN7HHZjLwrdMaNIoQ0+X11PqNLVRqTlnEzfiCUXM6HOW4pZa6PD5+qSyKOEGaTa3kS7Glg==",
"dev": true,
"requires": {
"case-anything": "^2.1.13",
diff --git a/modules/chronik-client/package.json b/modules/chronik-client/package.json
--- a/modules/chronik-client/package.json
+++ b/modules/chronik-client/package.json
@@ -1,6 +1,6 @@
{
"name": "chronik-client",
- "version": "0.14.0",
+ "version": "0.15.0",
"description": "A client for accessing the Chronik Indexer API",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -25,6 +25,7 @@
"@typescript-eslint/parser": "^5.5.0",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
+ "ecashaddrjs": "^1.5.6",
"eslint": "^8.37.0",
"eslint-config-prettier": "^8.3.0",
"mocha": "^9.1.3",
diff --git a/modules/chronik-client/src/ChronikClientNode.ts b/modules/chronik-client/src/ChronikClientNode.ts
--- a/modules/chronik-client/src/ChronikClientNode.ts
+++ b/modules/chronik-client/src/ChronikClientNode.ts
@@ -90,6 +90,73 @@
const rawTx = proto.RawTx.decode(data);
return convertToRawTx(rawTx);
}
+
+ /** Create object that allows fetching script history or UTXOs. */
+ public script(
+ scriptType: ScriptType_InNode,
+ scriptPayload: string,
+ ): ScriptEndpointInNode {
+ return new ScriptEndpointInNode(
+ this._proxyInterface,
+ scriptType,
+ scriptPayload,
+ );
+ }
+}
+
+/** Allows fetching script history and UTXOs. */
+export class ScriptEndpointInNode {
+ private _proxyInterface: FailoverProxy;
+ private _scriptType: string;
+ private _scriptPayload: string;
+
+ constructor(
+ proxyInterface: FailoverProxy,
+ scriptType: string,
+ scriptPayload: string,
+ ) {
+ this._proxyInterface = proxyInterface;
+ this._scriptType = scriptType;
+ this._scriptPayload = scriptPayload;
+ }
+
+ /**
+ * Fetches the tx history of this script, in anti-chronological order.
+ * This means it's ordered by first-seen first. If the tx hasn't been seen
+ * by the indexer before, it's ordered by the block timestamp.
+ * @param page Page index of the tx history.
+ * @param pageSize Number of txs per page.
+ */
+ public async history(
+ page = 0, // Get the first page if unspecified
+ pageSize = 25, // Must be less than 200, let server handle error as server setting could change
+ ): Promise<TxHistoryPage_InNode> {
+ const data = await this._proxyInterface.get(
+ `/script/${this._scriptType}/${this._scriptPayload}/history?page=${page}&page_size=${pageSize}`,
+ );
+ const historyPage = proto.TxHistoryPage.decode(data);
+ return {
+ txs: historyPage.txs.map(convertToTx),
+ numPages: historyPage.numPages,
+ numTxs: historyPage.numTxs,
+ };
+ }
+
+ /**
+ * Fetches the current UTXO set for this script.
+ * It is grouped by output script, in case a script type can match multiple
+ * different output scripts (e.g. Taproot on Lotus).
+ */
+ public async utxos(): Promise<ScriptUtxos_InNode> {
+ const data = await this._proxyInterface.get(
+ `/script/${this._scriptType}/${this._scriptPayload}/utxos`,
+ );
+ const scriptUtxos = proto.ScriptUtxos.decode(data);
+ return {
+ outputScript: toHex(scriptUtxos.script),
+ utxos: scriptUtxos.utxos.map(convertToUtxo),
+ };
+ }
}
function convertToBlockchainInfo(
@@ -210,6 +277,22 @@
};
}
+function convertToUtxo(utxo: proto.ScriptUtxo): Utxo_InNode {
+ if (utxo.outpoint === undefined) {
+ throw new Error('UTXO outpoint is undefined');
+ }
+ return {
+ outpoint: {
+ txid: toHexRev(utxo.outpoint.txid),
+ outIdx: utxo.outpoint.outIdx,
+ },
+ blockHeight: utxo.blockHeight,
+ isCoinbase: utxo.isCoinbase,
+ value: parseInt(utxo.value),
+ isFinal: utxo.isFinal,
+ };
+}
+
/** Info about connected chronik server */
export interface ChronikInfo {
version: string;
@@ -344,3 +427,40 @@
*/
timestamp: number;
}
+
+/** Group of UTXOs by output script. */
+export interface ScriptUtxos_InNode {
+ /** Output script in hex. */
+ outputScript: string;
+ /** UTXOs of the output script. */
+ utxos: Utxo_InNode[];
+}
+
+/** An unspent transaction output (aka. UTXO, aka. "Coin") of a script. */
+export interface Utxo_InNode {
+ /** Outpoint of the UTXO. */
+ outpoint: OutPoint;
+ /** Which block this UTXO is in, or -1 if in the mempool. */
+ blockHeight: number;
+ /** Whether this UTXO is a coinbase UTXO
+ * (make sure it's buried 100 blocks before spending!) */
+ isCoinbase: boolean;
+ /** Value of the UTXO in satoshis. */
+ value: number;
+ /** Is this utxo avalanche finalized */
+ isFinal: boolean;
+}
+
+/**
+ * Script type queried in the `script` method.
+ * - `other`: Script type not covered by the standard script types; payload is
+ * the raw hex.
+ * - `p2pk`: Pay-to-Public-Key (`<pk> OP_CHECKSIG`), payload is the hex of the
+ * pubkey (compressed (33 bytes) or uncompressed (65 bytes)).
+ * - `p2pkh`: Pay-to-Public-Key-Hash
+ * (`OP_DUP OP_HASH160 <pkh> OP_EQUALVERIFY OP_CHECKSIG`).
+ * Payload is the 20 byte public key hash.
+ * - `p2sh`: Pay-to-Script-Hash (`OP_HASH160 <sh> OP_EQUAL`).
+ * Payload is the 20 byte script hash.
+ */
+export type ScriptType_InNode = 'other' | 'p2pk' | 'p2pkh' | 'p2sh';
diff --git a/modules/chronik-client/test/integration/script_endpoints.ts b/modules/chronik-client/test/integration/script_endpoints.ts
new file mode 100644
--- /dev/null
+++ b/modules/chronik-client/test/integration/script_endpoints.ts
@@ -0,0 +1,687 @@
+import * as chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import cashaddr from 'ecashaddrjs';
+import { ChildProcess } from 'node:child_process';
+import { EventEmitter, once } from 'node:events';
+import { ChronikClientNode, ScriptType_InNode } from '../../index';
+import initializeTestRunner from '../setup/testRunner';
+
+const expect = chai.expect;
+chai.use(chaiAsPromised);
+
+describe('Get script().history and script().utxos()', () => {
+ let testRunner: ChildProcess;
+ let chronik_url: Promise<Array<string>>;
+ let get_txs_broadcast: Promise<string>;
+
+ let get_p2pkh_address: Promise<string>;
+ let get_p2pkh_txids: Promise<Array<string>>;
+
+ let get_p2sh_address: Promise<string>;
+ let get_p2sh_txids: Promise<Array<string>>;
+
+ let get_p2pk_script: Promise<string>;
+ let get_p2pk_txids: Promise<Array<string>>;
+
+ let get_other_script: Promise<string>;
+ let get_other_txids: Promise<Array<string>>;
+
+ let get_mixed_output_txid: Promise<string>;
+ const statusEvent = new EventEmitter();
+
+ before(async () => {
+ testRunner = initializeTestRunner(
+ 'chronik-client_script_utxos_and_history',
+ );
+
+ testRunner.on('message', function (message: any) {
+ if (message && message.chronik) {
+ console.log('Setting chronik url to ', message.chronik);
+ chronik_url = new Promise(resolve => {
+ resolve([message.chronik]);
+ });
+ }
+
+ if (message && message.txs_broadcast) {
+ get_txs_broadcast = new Promise(resolve => {
+ resolve(message.txs_broadcast);
+ });
+ }
+
+ if (message && message.p2pkh_address) {
+ get_p2pkh_address = new Promise(resolve => {
+ resolve(message.p2pkh_address);
+ });
+ }
+
+ if (message && message.p2sh_address) {
+ get_p2sh_address = new Promise(resolve => {
+ resolve(message.p2sh_address);
+ });
+ }
+
+ if (message && message.p2pk_script) {
+ get_p2pk_script = new Promise(resolve => {
+ resolve(message.p2pk_script);
+ });
+ }
+
+ if (message && message.other_script) {
+ get_other_script = new Promise(resolve => {
+ resolve(message.other_script);
+ });
+ }
+
+ if (message && message.p2pk_txids) {
+ get_p2pk_txids = new Promise(resolve => {
+ resolve(message.p2pk_txids);
+ });
+ }
+
+ if (message && message.p2pkh_txids) {
+ get_p2pkh_txids = new Promise(resolve => {
+ resolve(message.p2pkh_txids);
+ });
+ }
+
+ if (message && message.p2sh_txids) {
+ get_p2sh_txids = new Promise(resolve => {
+ resolve(message.p2sh_txids);
+ });
+ }
+
+ if (message && message.other_txids) {
+ get_other_txids = new Promise(resolve => {
+ resolve(message.other_txids);
+ });
+ }
+
+ if (message && message.mixed_output_txid) {
+ get_mixed_output_txid = new Promise(resolve => {
+ resolve(message.mixed_output_txid);
+ });
+ }
+
+ if (message && message.status) {
+ statusEvent.emit(message.status);
+ }
+ });
+ });
+
+ after(() => {
+ testRunner.send('stop');
+ });
+
+ beforeEach(async () => {
+ await once(statusEvent, 'ready');
+ });
+
+ afterEach(() => {
+ testRunner.send('next');
+ });
+
+ const REGTEST_CHAIN_INIT_HEIGHT = 200;
+
+ // Will get these values from node ipc, then use in multiple steps
+ let chronikUrl = [''];
+ let txsBroadcast = 0;
+
+ let p2pkhAddress = '';
+ let p2pkhAddressHash = '';
+ let p2pkhTxids: string[] = [];
+
+ let p2shAddress = '';
+ let p2shAddressHash = '';
+ let p2shTxids: string[] = [];
+
+ let p2pkScript = '';
+ let p2pkScriptBytecountHex = '00';
+ let p2pkTxids: string[] = [];
+
+ let otherScript = '';
+ let otherTxids: string[] = [];
+
+ it('New regtest chain', async () => {
+ // Get chronik URL (used in all tests)
+ chronikUrl = await chronik_url;
+ const chronik = new ChronikClientNode(chronikUrl);
+
+ // Get addresses / scripts (used in all tests)
+ p2pkhAddress = await get_p2pkh_address;
+ p2shAddress = await get_p2sh_address;
+ p2pkScript = await get_p2pk_script;
+ otherScript = await get_other_script;
+
+ // Get hashes for addresses (used in all tests)
+ const decodedP2pkh = cashaddr.decode(p2pkhAddress, true);
+ if (typeof decodedP2pkh.hash === 'string') {
+ p2pkhAddressHash = decodedP2pkh.hash;
+ }
+ const decodedP2sh = cashaddr.decode(p2shAddress, true);
+ if (typeof decodedP2sh.hash === 'string') {
+ p2shAddressHash = decodedP2sh.hash;
+ }
+
+ const checkEmptyHistoryAndUtxos = async (
+ chronik: ChronikClientNode,
+ type: ScriptType_InNode,
+ payload: string,
+ expectedOutputScript: string,
+ ) => {
+ const chronikScript = chronik.script(type, payload);
+ const history = await chronikScript.history();
+ const utxos = await chronikScript.utxos();
+
+ // Expect empty history
+ expect(history).to.deep.equal({ txs: [], numPages: 0, numTxs: 0 });
+
+ // Hash is returned at the outputScript key, no utxos
+ expect(utxos).to.deep.equal({
+ outputScript: expectedOutputScript,
+ utxos: [],
+ });
+
+ console.log('\x1b[32m%s\x1b[0m', `✔ ${type}`);
+ };
+
+ // p2pkh
+ await checkEmptyHistoryAndUtxos(
+ chronik,
+ 'p2pkh',
+ p2pkhAddressHash,
+ cashaddr.getOutputScriptFromAddress(p2pkhAddress),
+ );
+
+ // p2sh
+ await checkEmptyHistoryAndUtxos(
+ chronik,
+ 'p2sh',
+ p2shAddressHash,
+ cashaddr.getOutputScriptFromAddress(p2shAddress),
+ );
+
+ // p2pk
+ p2pkScriptBytecountHex = (p2pkScript.length / 2).toString(16);
+ await checkEmptyHistoryAndUtxos(
+ chronik,
+ 'p2pk',
+ p2pkScript,
+ `${p2pkScriptBytecountHex}${p2pkScript}ac`,
+ );
+
+ // other
+ await checkEmptyHistoryAndUtxos(
+ chronik,
+ 'other',
+ otherScript,
+ otherScript,
+ );
+
+ // Expected errors
+ const checkExpectedErrors = async (
+ chronik: ChronikClientNode,
+ type: ScriptType_InNode,
+ ) => {
+ const nonHexPayload = 'justsomestring';
+ const chronikScriptNonHexPayload = chronik.script(
+ type,
+ nonHexPayload,
+ );
+ await expect(
+ chronikScriptNonHexPayload.history(),
+ ).to.be.rejectedWith(
+ Error,
+ `Failed getting /script/${type}/${nonHexPayload}/history?page=0&page_size=25 (): 400: Invalid hex: Invalid character '${nonHexPayload[0]}' at position 0`,
+ );
+ await expect(chronikScriptNonHexPayload.utxos()).to.be.rejectedWith(
+ Error,
+ `Failed getting /script/${type}/${nonHexPayload}/utxos (): 400: Invalid hex: Invalid character '${nonHexPayload[0]}' at position 0`,
+ );
+
+ const hexPayload = 'deadbeef';
+ const chronikScriptHexPayload = chronik.script(type, hexPayload);
+
+ if (type === 'p2pkh' || type == 'p2sh') {
+ await expect(
+ chronikScriptHexPayload.history(),
+ ).to.be.rejectedWith(
+ Error,
+ `Failed getting /script/${type}/${hexPayload}/history?page=0&page_size=25 (): 400: Invalid payload for ${type.toUpperCase()}: Invalid length, expected 20 bytes but got 4 bytes`,
+ );
+ await expect(
+ chronikScriptHexPayload.utxos(),
+ ).to.be.rejectedWith(
+ Error,
+ `Failed getting /script/${type}/${hexPayload}/utxos (): 400: Invalid payload for ${type.toUpperCase()}: Invalid length, expected 20 bytes but got 4 bytes`,
+ );
+ }
+ if (type === 'p2pk') {
+ await expect(
+ chronikScriptHexPayload.history(),
+ ).to.be.rejectedWith(
+ Error,
+ `Failed getting /script/${type}/${hexPayload}/history?page=0&page_size=25 (): 400: Invalid payload for ${type.toUpperCase()}: Invalid length, expected one of [33, 65] but got 4 bytes`,
+ );
+ await expect(
+ chronikScriptHexPayload.utxos(),
+ ).to.be.rejectedWith(
+ Error,
+ `Failed getting /script/${type}/${hexPayload}/utxos (): 400: Invalid payload for ${type.toUpperCase()}: Invalid length, expected one of [33, 65] but got 4 bytes`,
+ );
+ }
+
+ console.log(
+ '\x1b[32m%s\x1b[0m',
+ `✔ ${type} throws expected errors`,
+ );
+ };
+ await checkExpectedErrors(chronik, 'p2pkh');
+ await checkExpectedErrors(chronik, 'p2sh');
+ await checkExpectedErrors(chronik, 'p2pk');
+ await checkExpectedErrors(chronik, 'other');
+
+ // 'other' endpoint will not throw an error on ridiculously long valid hex
+ // 440 bytes
+ const outTherePayload =
+ 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef';
+ await checkEmptyHistoryAndUtxos(
+ chronik,
+ 'other',
+ outTherePayload,
+ outTherePayload,
+ );
+ });
+
+ it('After some txs have been broadcast', async () => {
+ txsBroadcast = parseInt(await get_txs_broadcast);
+
+ const chronik = new ChronikClientNode(chronikUrl);
+
+ const checkHistoryAndUtxosInMempool = async (
+ chronik: ChronikClientNode,
+ type: ScriptType_InNode,
+ payload: string,
+ expectedOutputScript: string,
+ broadcastTxids: string[],
+ ) => {
+ const chronikScript = chronik.script(type, payload);
+ // Use broadcastTxids.length for page size, so that we can be sure the first page has all the txs
+ // Test pagination separately
+ const history = await chronikScript.history(
+ 0,
+ broadcastTxids.length,
+ );
+ const utxos = await chronikScript.utxos();
+
+ // fetched history tx count is the same as txids broadcast to this address
+ expect(history.numTxs).to.eql(broadcastTxids.length);
+
+ const historyTxids = [];
+ for (const fetchedHistoryTx of history.txs) {
+ historyTxids.push(fetchedHistoryTx.txid);
+ // The 'block' key is undefined, denoting an unconfirmed tx
+ expect(typeof fetchedHistoryTx.block).to.eql('undefined');
+ }
+
+ // txids fetched from history match what the node broadcast
+ expect(historyTxids).to.have.members(broadcastTxids);
+
+ // The returned outputScript matches the calling script hash
+ expect(utxos.outputScript).to.eql(expectedOutputScript);
+
+ // We have as many utxos as there were txs sent to this address
+ expect(utxos.utxos.length).to.eql(broadcastTxids.length);
+
+ const utxoTxids = [];
+ for (const utxo of utxos.utxos) {
+ // Each utxo is unconfirmed, denoted by a value of -1 at the blockHeight key
+ expect(utxo.blockHeight).to.eql(-1);
+ // The utxo is not from a finalized tx
+ expect(utxo.isFinal).to.eql(false);
+ utxoTxids.push(utxo.outpoint.txid);
+ }
+
+ // utxos fetched from history match what the node broadcast
+ expect(utxoTxids).to.have.members(broadcastTxids);
+
+ console.log('\x1b[32m%s\x1b[0m', `✔ ${type}`);
+ };
+
+ // p2pkh
+ p2pkhTxids = await get_p2pkh_txids;
+ await checkHistoryAndUtxosInMempool(
+ chronik,
+ 'p2pkh',
+ p2pkhAddressHash,
+ cashaddr.getOutputScriptFromAddress(p2pkhAddress),
+ p2pkhTxids,
+ );
+
+ // p2sh
+ p2shTxids = await get_p2sh_txids;
+ await checkHistoryAndUtxosInMempool(
+ chronik,
+ 'p2sh',
+ p2shAddressHash,
+ cashaddr.getOutputScriptFromAddress(p2shAddress),
+ p2shTxids,
+ );
+
+ // p2pk
+ p2pkTxids = await get_p2pk_txids;
+ await checkHistoryAndUtxosInMempool(
+ chronik,
+ 'p2pk',
+ p2pkScript,
+ `${p2pkScriptBytecountHex}${p2pkScript}ac`,
+ p2pkTxids,
+ );
+
+ // other
+ otherTxids = await get_other_txids;
+ await checkHistoryAndUtxosInMempool(
+ chronik,
+ 'other',
+ otherScript,
+ otherScript,
+ otherTxids,
+ );
+
+ const checkPagination = async (
+ chronik: ChronikClientNode,
+ type: ScriptType_InNode,
+ payload: string,
+ txsBroadcast: number,
+ customPageSize: number,
+ ) => {
+ const chronikScript = chronik.script(type, payload);
+ // We can customize pageSize for history
+ const customPageSizeHistory = await chronikScript.history(
+ 0,
+ customPageSize,
+ );
+
+ if (customPageSize <= txsBroadcast) {
+ expect(customPageSizeHistory.txs.length).to.eql(customPageSize);
+ } else {
+ expect(customPageSizeHistory.txs.length).to.eql(txsBroadcast);
+ }
+
+ // We expect enough pages for the full history
+ expect(customPageSizeHistory.numPages).to.eql(
+ Math.ceil(txsBroadcast / customPageSize),
+ );
+ // Note, the first page is page 0
+ // The last page is numPages - 1
+ const lastPage = customPageSizeHistory.numPages - 1;
+ const lastPageHistoryCustomPageSize = await chronik
+ .script(type, payload)
+ .history(lastPage, customPageSize);
+ const expectedEntriesOnLastPage =
+ txsBroadcast % customPageSize === 0
+ ? customPageSize
+ : txsBroadcast % customPageSize;
+ expect(lastPageHistoryCustomPageSize.txs.length).to.eql(
+ expectedEntriesOnLastPage,
+ );
+
+ // If we ask for a page number higher than numPages, we get an empty array of txs
+ const emptyPage = await chronik
+ .script(type, payload)
+ .history(lastPage + 1, customPageSize);
+ expect(emptyPage.txs.length).to.eql(0);
+
+ // We cannot use pageSize of 0
+ await expect(chronikScript.history(0, 0)).to.be.rejectedWith(
+ Error,
+ `Failed getting /script/${type}/${payload}/history?page=0&page_size=0 (): 400: Requested page size 0 is too small, minimum is 1`,
+ );
+
+ console.log('\x1b[32m%s\x1b[0m', `✔ ${type} pagination`);
+ };
+
+ // p2pkh pagination
+ await checkPagination(
+ chronik,
+ 'p2pkh',
+ p2pkhAddressHash,
+ txsBroadcast,
+ 3,
+ );
+ // p2sh pagination
+ await checkPagination(
+ chronik,
+ 'p2sh',
+ p2shAddressHash,
+ txsBroadcast,
+ 1,
+ );
+ // p2pk pagination
+ await checkPagination(chronik, 'p2pk', p2pkScript, txsBroadcast, 20);
+ // other pagination
+ await checkPagination(chronik, 'other', otherScript, txsBroadcast, 50);
+ });
+ it('After these txs are mined', async () => {
+ const chronik = new ChronikClientNode(chronikUrl);
+
+ const checkHistoryAndUtxosAfterConfirmation = async (
+ chronik: ChronikClientNode,
+ type: ScriptType_InNode,
+ payload: string,
+ expectedOutputScript: string,
+ broadcastTxids: string[],
+ ) => {
+ const chronikScript = chronik.script(type, payload);
+ // Use broadcastTxids.length for page size, so that we can be sure the first page has all the txs
+ // Test pagination separately
+ const history = await chronikScript.history(
+ 0,
+ broadcastTxids.length,
+ );
+ const utxos = await chronikScript.utxos();
+
+ // fetched history tx count is the same as txids broadcast to this address
+ expect(history.numTxs).to.eql(broadcastTxids.length);
+
+ const historyTxids = [];
+ for (const fetchedHistoryTx of history.txs) {
+ historyTxids.push(fetchedHistoryTx.txid);
+ // We now have a blockheight
+ expect(fetchedHistoryTx.block?.height).to.eql(
+ REGTEST_CHAIN_INIT_HEIGHT + 1,
+ );
+ }
+
+ // txids fetched from history match what the node broadcast
+ expect(historyTxids).to.have.members(broadcastTxids);
+
+ // The returned outputScript matches the calling script hash
+ expect(utxos.outputScript).to.eql(expectedOutputScript);
+
+ // We have as many utxos as there were txs sent to this address
+ expect(utxos.utxos.length).to.eql(broadcastTxids.length);
+
+ const utxoTxids = [];
+ for (const utxo of utxos.utxos) {
+ // Each utxo is now confirmed at the right blockheight
+ expect(utxo.blockHeight).to.eql(REGTEST_CHAIN_INIT_HEIGHT + 1);
+ // The utxo is not from a finalized tx
+ expect(utxo.isFinal).to.eql(false);
+ utxoTxids.push(utxo.outpoint.txid);
+ }
+
+ // utxos fetched from history match what the node broadcast
+ expect(utxoTxids).to.have.members(broadcastTxids);
+
+ console.log('\x1b[32m%s\x1b[0m', `✔ ${type}`);
+ };
+ // p2pkh
+ await checkHistoryAndUtxosAfterConfirmation(
+ chronik,
+ 'p2pkh',
+ p2pkhAddressHash,
+ cashaddr.getOutputScriptFromAddress(p2pkhAddress),
+ p2pkhTxids,
+ );
+
+ // p2sh
+ await checkHistoryAndUtxosAfterConfirmation(
+ chronik,
+ 'p2sh',
+ p2shAddressHash,
+ cashaddr.getOutputScriptFromAddress(p2shAddress),
+ p2shTxids,
+ );
+
+ // p2pk
+ await checkHistoryAndUtxosAfterConfirmation(
+ chronik,
+ 'p2pk',
+ p2pkScript,
+ `${p2pkScriptBytecountHex}${p2pkScript}ac`,
+ p2pkTxids,
+ );
+
+ // other
+ await checkHistoryAndUtxosAfterConfirmation(
+ chronik,
+ 'other',
+ otherScript,
+ otherScript,
+ otherTxids,
+ );
+ });
+ it('After these txs are avalanche finalized', async () => {
+ // Note: no change is expected from script().history() for this case
+ // as 'isFinal' is present only on utxos
+ // Potential TODO, add isFinal key to tx proto in chronik
+
+ const chronik = new ChronikClientNode(chronikUrl);
+
+ const checkAvalancheFinalized = async (
+ chronik: ChronikClientNode,
+ type: ScriptType_InNode,
+ payload: string,
+ expectedOutputScript: string,
+ broadcastTxids: string[],
+ ) => {
+ const chronikScript = chronik.script(type, payload);
+ const utxos = await chronikScript.utxos();
+
+ // The returned outputScript matches the calling script hash
+ expect(utxos.outputScript).to.eql(expectedOutputScript);
+
+ // We have as many utxos as there were txs sent to this address
+ expect(utxos.utxos.length).to.eql(broadcastTxids.length);
+
+ const utxoTxids = [];
+ for (const utxo of utxos.utxos) {
+ // Each utxo is now confirmed at the right blockheight
+ expect(utxo.blockHeight).to.eql(REGTEST_CHAIN_INIT_HEIGHT + 1);
+ // The utxo is now marked as finalized by Avalanche
+ expect(utxo.isFinal).to.eql(true);
+ utxoTxids.push(utxo.outpoint.txid);
+ }
+
+ // utxos fetched from history match what the node broadcast
+ expect(utxoTxids).to.have.members(broadcastTxids);
+
+ console.log('\x1b[32m%s\x1b[0m', `✔ ${type}`);
+ };
+
+ // p2pkh
+ await checkAvalancheFinalized(
+ chronik,
+ 'p2pkh',
+ p2pkhAddressHash,
+ cashaddr.getOutputScriptFromAddress(p2pkhAddress),
+ p2pkhTxids,
+ );
+
+ // p2sh
+ await checkAvalancheFinalized(
+ chronik,
+ 'p2sh',
+ p2shAddressHash,
+ cashaddr.getOutputScriptFromAddress(p2shAddress),
+ p2shTxids,
+ );
+
+ // p2pk
+ await checkAvalancheFinalized(
+ chronik,
+ 'p2pk',
+ p2pkScript,
+ `${p2pkScriptBytecountHex}${p2pkScript}ac`,
+ p2pkTxids,
+ );
+
+ // other
+ await checkAvalancheFinalized(
+ chronik,
+ 'other',
+ otherScript,
+ otherScript,
+ otherTxids,
+ );
+ });
+ it('After a tx is broadcast with outputs of each type', async () => {
+ const chronik = new ChronikClientNode(chronikUrl);
+ const mixedTxid = await get_mixed_output_txid;
+
+ const checkMixedTxInHistory = async (
+ chronik: ChronikClientNode,
+ type: ScriptType_InNode,
+ payload: string,
+ mixedTxid: string,
+ txsBroadcast: number,
+ ) => {
+ const chronikScript = chronik.script(type, payload);
+ const history = await chronikScript.history();
+ const utxos = await chronikScript.utxos();
+ // We see a new tx in numTxs count
+ expect(history.numTxs).to.eql(txsBroadcast + 1);
+ // The most recent txid appears at the first element of the tx history array
+ expect(history.txs[0].txid).to.eql(mixedTxid);
+ // The most recent txid appears at the last element of the utxos array
+ expect(utxos.utxos[utxos.utxos.length - 1].outpoint.txid).to.eql(
+ mixedTxid,
+ );
+ console.log(
+ `${type} script endpoints registed tx with mixed outputs`,
+ );
+ };
+
+ await checkMixedTxInHistory(
+ chronik,
+ 'p2pkh',
+ p2pkhAddressHash,
+ mixedTxid,
+ txsBroadcast,
+ );
+
+ await checkMixedTxInHistory(
+ chronik,
+ 'p2sh',
+ p2shAddressHash,
+ mixedTxid,
+ txsBroadcast,
+ );
+
+ await checkMixedTxInHistory(
+ chronik,
+ 'p2pk',
+ p2pkScript,
+ mixedTxid,
+ txsBroadcast,
+ );
+
+ await checkMixedTxInHistory(
+ chronik,
+ 'other',
+ otherScript,
+ mixedTxid,
+ txsBroadcast,
+ );
+ });
+});
diff --git a/test/functional/setup_scripts/chronik-client_script_utxos_and_history.py b/test/functional/setup_scripts/chronik-client_script_utxos_and_history.py
new file mode 100644
--- /dev/null
+++ b/test/functional/setup_scripts/chronik-client_script_utxos_and_history.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+# Copyright (c) 2024 The Bitcoin developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""
+Setup script to exercise the chronik-client js library script endpoints
+"""
+
+import pathmagic # noqa
+from ipc import send_ipc_message
+from setup_framework import SetupFramework
+from test_framework.avatools import AvaP2PInterface, can_find_inv_in_poll
+from test_framework.messages import CTransaction, CTxOut, FromHex, ToHex
+from test_framework.script import OP_CHECKSIG, CScript
+from test_framework.util import assert_equal
+
+QUORUM_NODE_COUNT = 16
+
+
+class ChronikClient_Block_Setup(SetupFramework):
+ def set_test_params(self):
+ self.num_nodes = 1
+ self.extra_args = [
+ [
+ "-chronik",
+ "-avaproofstakeutxodustthreshold=1000000",
+ "-avaproofstakeutxoconfirmations=1",
+ "-avacooldown=0",
+ "-avaminquorumstake=0",
+ "-avaminavaproofsnodecount=0",
+ "-persistavapeers=0",
+ "-acceptnonstdtxn=1",
+ ]
+ ]
+ self.ipc_timeout = 10
+
+ def skip_test_if_missing_module(self):
+ self.skip_if_no_chronik()
+ self.skip_if_no_wallet()
+
+ def send_chronik_info(self):
+ send_ipc_message({"chronik": f"http://127.0.0.1:{self.nodes[0].chronik_port}"})
+
+ def run_test(self):
+ # Init
+ node = self.nodes[0]
+
+ self.send_chronik_info()
+
+ # p2pkh
+ # IFP address p2pkh
+ # Note: we use this instead of node.getnewaddress() so we don't get change
+ # to our p2pkh address from p2sh txs, causing chronik to give hard-to-predict
+ # results (txs with mixed script outputs will come up in each)
+ p2pkh_address = "ecregtest:qrfhcnyqnl5cgrnmlfmms675w93ld7mvvqjh9pgptw"
+ p2pkh_output_script = bytes.fromhex(
+ "76a914d37c4c809fe9840e7bfa77b86bd47163f6fb6c6088ac"
+ )
+ send_ipc_message({"p2pkh_address": p2pkh_address})
+
+ # p2sh
+ # use IFP address p2sh
+ p2sh_address = "ecregtest:prfhcnyqnl5cgrnmlfmms675w93ld7mvvq9jcw0zsn"
+ p2sh_output_script = bytes.fromhex(
+ "a914d37c4c809fe9840e7bfa77b86bd47163f6fb6c6087"
+ )
+ send_ipc_message({"p2sh_address": p2sh_address})
+
+ # p2pk
+ # See coinbase tx of coinbase tx output from https://explorer.e.cash/block/00000000000000002328cef155f92bf149cfbe365eecf4e428f2c11f25fcce56
+ pubkey = bytes.fromhex(
+ "047fa64f6874fb7213776b24c40bc915451b57ef7f17ad7b982561f99f7cdc7010d141b856a092ee169c5405323895e1962c6b0d7c101120d360164c9e4b3997bd"
+ )
+ send_ipc_message({"p2pk_script": pubkey.hex()})
+ p2pk_script_for_tx_building = CScript([pubkey, OP_CHECKSIG])
+
+ # other
+ # Use hex deadbeef
+ other_script = "deadbeef"
+ send_ipc_message({"other_script": other_script})
+ other_script_for_tx_building = bytes.fromhex(other_script)
+ yield True
+
+ self.log.info("Step 1: Broadcast txs to a p2pk, p2pkh, and p2sh address")
+ # Set the number of txs you wish to broadcast
+ # Tested up to 100, takes 25s
+ # 200 goes over regtest 60s timeout
+ # Must be <= 200 since the JS tests match items on first page and
+ # this is chronik's max per-page count
+ # 25 takes about 10s. Default page size for chronik.
+ txs_broadcast = 25
+ send_ipc_message({"txs_broadcast": txs_broadcast})
+
+ # p2pkh
+ p2pkh_txids = []
+ for x in range(txs_broadcast):
+ p2pkh_txid = node.sendtoaddress(p2pkh_address, (x + 1) * 1000)
+ p2pkh_txids.append(p2pkh_txid)
+ send_ipc_message({"p2pkh_txids": p2pkh_txids})
+
+ # p2sh
+ p2sh_txids = []
+ for x in range(txs_broadcast):
+ p2sh_txid = node.sendtoaddress(p2sh_address, (x + 1) * 1000)
+ p2sh_txids.append(p2sh_txid)
+ send_ipc_message({"p2sh_txids": p2sh_txids})
+
+ # p2pk
+ p2pk_txids = []
+ for x in range(txs_broadcast):
+ tx = CTransaction()
+ tx.vout.append(CTxOut((x + 1) * 1000, p2pk_script_for_tx_building))
+ rawtx = node.fundrawtransaction(
+ ToHex(tx),
+ )["hex"]
+ FromHex(tx, rawtx)
+ rawtx = node.signrawtransactionwithwallet(ToHex(tx))["hex"]
+ p2pk_txid = node.sendrawtransaction(rawtx)
+ p2pk_txids.append(p2pk_txid)
+ send_ipc_message({"p2pk_txids": p2pk_txids})
+
+ # other
+ other_txids = []
+ for x in range(txs_broadcast):
+ tx = CTransaction()
+ tx.vout.append(CTxOut((x + 1) * 1000, other_script_for_tx_building))
+ rawtx = node.fundrawtransaction(
+ ToHex(tx),
+ )["hex"]
+ FromHex(tx, rawtx)
+ rawtx = node.signrawtransactionwithwallet(ToHex(tx))["hex"]
+ other_txid = node.sendrawtransaction(rawtx)
+ other_txids.append(other_txid)
+ send_ipc_message({"other_txids": other_txids})
+ assert_equal(node.getblockcount(), 200)
+ yield True
+
+ self.log.info("Step 2: Mine a block with these txs")
+ self.generate(node, 1)
+ assert_equal(node.getblockcount(), 201)
+ yield True
+
+ self.log.info("Step 3: Avalanche finalize a block with these txs")
+
+ # Build a fake quorum of nodes.
+ def get_quorum():
+ return [
+ node.add_p2p_connection(AvaP2PInterface(self, node))
+ for _ in range(0, QUORUM_NODE_COUNT)
+ ]
+
+ # Pick one node from the quorum for polling.
+ quorum = get_quorum()
+
+ def is_quorum_established():
+ return node.getavalancheinfo()["ready_to_poll"] is True
+
+ self.wait_until(is_quorum_established)
+
+ blockhash = self.generate(node, 1, sync_fun=self.no_op)[0]
+ cb_txid = node.getblock(blockhash)["tx"][0]
+ assert not node.isfinalblock(blockhash)
+ assert not node.isfinaltransaction(cb_txid, blockhash)
+
+ def is_finalblock(blockhash):
+ can_find_inv_in_poll(quorum, int(blockhash, 16))
+ return node.isfinalblock(blockhash)
+
+ with node.assert_debug_log([f"Avalanche finalized block {blockhash}"]):
+ self.wait_until(lambda: is_finalblock(blockhash))
+ assert node.isfinaltransaction(cb_txid, blockhash)
+ yield True
+
+ self.log.info("Step 4: Broadcast a tx with mixed outputs")
+ mixed_output_tx = CTransaction()
+ mixed_output_tx.vout.append(CTxOut(1000000, p2pkh_output_script))
+ mixed_output_tx.vout.append(CTxOut(1000000, p2sh_output_script))
+ mixed_output_tx.vout.append(CTxOut(1000000, p2pk_script_for_tx_building))
+ mixed_output_tx.vout.append(CTxOut(1000000, other_script_for_tx_building))
+ mixed_output_rawtx = node.fundrawtransaction(
+ ToHex(mixed_output_tx),
+ )["hex"]
+ FromHex(mixed_output_tx, mixed_output_rawtx)
+ mixed_output_rawtx = node.signrawtransactionwithwallet(ToHex(mixed_output_tx))[
+ "hex"
+ ]
+ mixed_output_txid = node.sendrawtransaction(mixed_output_rawtx)
+ send_ipc_message({"mixed_output_txid": mixed_output_txid})
+ yield True
+
+
+if __name__ == "__main__":
+ ChronikClient_Block_Setup().main()

File Metadata

Mime Type
text/plain
Expires
Thu, Feb 6, 16:53 (18 h, 10 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5082727
Default Alt Text
D15392.id45096.diff (57 KB)

Event Timeline