Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F12944990
D15392.id45096.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
57 KB
Subscribers
None
D15392.id45096.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Thu, Feb 6, 16:53 (20 h, 45 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5082727
Default Alt Text
D15392.id45096.diff (57 KB)
Attached To
D15392: [chronik-client] Support for script endpoints
Event Timeline
Log In to Comment