@did-btcr2/method 0.58.0 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +3 -3
- package/dist/browser.mjs +3 -3
- package/dist/cjs/index.js +985 -753
- package/dist/esm/core/beacon/beacon.js +8 -1
- package/dist/esm/core/beacon/beacon.js.map +1 -1
- package/dist/esm/core/beacon/signal-discovery.js +24 -3
- package/dist/esm/core/beacon/signal-discovery.js.map +1 -1
- package/dist/esm/core/identifier.js +175 -23
- package/dist/esm/core/identifier.js.map +1 -1
- package/dist/esm/core/resolver.js +77 -3
- package/dist/esm/core/resolver.js.map +1 -1
- package/dist/esm/did-btcr2.js +2 -1
- package/dist/esm/did-btcr2.js.map +1 -1
- package/dist/types/core/beacon/beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/signal-discovery.d.ts +14 -0
- package/dist/types/core/beacon/signal-discovery.d.ts.map +1 -1
- package/dist/types/core/identifier.d.ts +67 -0
- package/dist/types/core/identifier.d.ts.map +1 -1
- package/dist/types/core/interfaces.d.ts +13 -0
- package/dist/types/core/interfaces.d.ts.map +1 -1
- package/dist/types/core/resolver.d.ts +13 -0
- package/dist/types/core/resolver.d.ts.map +1 -1
- package/dist/types/did-btcr2.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/core/beacon/beacon.ts +14 -3
- package/src/core/beacon/signal-discovery.ts +25 -3
- package/src/core/identifier.ts +267 -22
- package/src/core/interfaces.ts +13 -0
- package/src/core/resolver.ts +87 -3
- package/src/did-btcr2.ts +2 -1
package/dist/cjs/index.js
CHANGED
|
@@ -32,6 +32,7 @@ __export(index_exports, {
|
|
|
32
32
|
CASBeaconError: () => CASBeaconError,
|
|
33
33
|
CHANGE_OUTPUT_VBYTES: () => CHANGE_OUTPUT_VBYTES,
|
|
34
34
|
DEFAULT_FEE_ESTIMATOR: () => DEFAULT_FEE_ESTIMATOR,
|
|
35
|
+
DEFAULT_MIN_CONF: () => DEFAULT_MIN_CONF,
|
|
35
36
|
DID_REGEX: () => DID_REGEX,
|
|
36
37
|
DUST_LIMIT_SATS: () => DUST_LIMIT_SATS,
|
|
37
38
|
DidBtcr2: () => DidBtcr2,
|
|
@@ -169,6 +170,9 @@ function opReturnScript(signalBytes) {
|
|
|
169
170
|
return import_btc_signer.Script.encode(["RETURN", signalBytes]);
|
|
170
171
|
}
|
|
171
172
|
var SPENDABLE_DUST_LIMIT_SATS = 546;
|
|
173
|
+
function isConfirmedUtxo(utxo) {
|
|
174
|
+
return utxo.status.confirmed === true;
|
|
175
|
+
}
|
|
172
176
|
function byDepthThenId(a, b) {
|
|
173
177
|
if (a.status.block_height !== b.status.block_height) {
|
|
174
178
|
return a.status.block_height - b.status.block_height;
|
|
@@ -184,7 +188,7 @@ function selectSpendableUtxo(utxos, address) {
|
|
|
184
188
|
{ address }
|
|
185
189
|
);
|
|
186
190
|
}
|
|
187
|
-
const confirmed = utxos.filter(
|
|
191
|
+
const confirmed = utxos.filter(isConfirmedUtxo);
|
|
188
192
|
const spendable = confirmed.filter((utxo) => utxo.value > SPENDABLE_DUST_LIMIT_SATS);
|
|
189
193
|
if (!spendable.length) {
|
|
190
194
|
const reason = confirmed.length === 0 ? `all ${utxos.length} UTXO(s) are unconfirmed` : `all ${confirmed.length} confirmed UTXO(s) are at or below the ${SPENDABLE_DUST_LIMIT_SATS}-sat dust limit`;
|
|
@@ -707,13 +711,13 @@ var BeaconFactory = class {
|
|
|
707
711
|
};
|
|
708
712
|
|
|
709
713
|
// src/core/beacon/signal-discovery.ts
|
|
710
|
-
var
|
|
711
|
-
var
|
|
714
|
+
var import_bitcoin4 = require("@did-btcr2/bitcoin");
|
|
715
|
+
var import_common10 = require("@did-btcr2/common");
|
|
712
716
|
|
|
713
717
|
// src/core/beacon/utils.ts
|
|
714
|
-
var
|
|
715
|
-
var
|
|
716
|
-
var
|
|
718
|
+
var import_bitcoin3 = require("@did-btcr2/bitcoin");
|
|
719
|
+
var import_common9 = require("@did-btcr2/common");
|
|
720
|
+
var import_btc_signer3 = require("@scure/btc-signer");
|
|
717
721
|
|
|
718
722
|
// src/utils/appendix.ts
|
|
719
723
|
var import_dids = require("@web5/dids");
|
|
@@ -895,883 +899,1048 @@ var Appendix = class _Appendix {
|
|
|
895
899
|
};
|
|
896
900
|
|
|
897
901
|
// src/core/identifier.ts
|
|
902
|
+
var import_common8 = require("@did-btcr2/common");
|
|
903
|
+
var import_keypair2 = require("@did-btcr2/keypair");
|
|
904
|
+
var import_utils4 = require("@noble/curves/utils.js");
|
|
905
|
+
var import_base = require("@scure/base");
|
|
906
|
+
|
|
907
|
+
// src/utils/did-document.ts
|
|
908
|
+
var import_bitcoin2 = require("@did-btcr2/bitcoin");
|
|
898
909
|
var import_common7 = require("@did-btcr2/common");
|
|
899
910
|
var import_keypair = require("@did-btcr2/keypair");
|
|
900
|
-
var
|
|
901
|
-
var
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
* @returns {string} The new did:btcr2 identifier.
|
|
915
|
-
*/
|
|
916
|
-
static encode(genesisBytes, options) {
|
|
917
|
-
const { idType, version = 1, network = "bitcoin" } = options;
|
|
918
|
-
if (!(idType in import_common7.IdentifierTypes)) {
|
|
919
|
-
throw new import_common7.IdentifierError('Expected "idType" to be "KEY" or "EXTERNAL"', import_common7.INVALID_DID, { idType });
|
|
920
|
-
}
|
|
921
|
-
if (version !== 1) {
|
|
922
|
-
throw new import_common7.IdentifierError('Expected "version" to be 1', import_common7.INVALID_DID, { version });
|
|
923
|
-
}
|
|
924
|
-
if (typeof network !== "string") {
|
|
925
|
-
throw new import_common7.IdentifierError('Expected "network" to be a known network name', import_common7.INVALID_DID, { network });
|
|
926
|
-
}
|
|
927
|
-
const networkValue = import_common7.BitcoinNetworkNames[network];
|
|
928
|
-
if (networkValue === void 0) {
|
|
929
|
-
throw new import_common7.IdentifierError('Invalid "network" name', import_common7.INVALID_DID, { network });
|
|
930
|
-
}
|
|
931
|
-
if (idType === "KEY") {
|
|
932
|
-
try {
|
|
933
|
-
new import_keypair.CompressedSecp256k1PublicKey(genesisBytes);
|
|
934
|
-
} catch {
|
|
935
|
-
throw new import_common7.IdentifierError(
|
|
936
|
-
'Expected "genesisBytes" to be a valid compressed secp256k1 public key',
|
|
937
|
-
import_common7.INVALID_DID,
|
|
938
|
-
{ genesisBytes }
|
|
939
|
-
);
|
|
940
|
-
}
|
|
941
|
-
} else if (genesisBytes.length !== 32) {
|
|
942
|
-
throw new import_common7.IdentifierError(
|
|
943
|
-
'Expected "genesisBytes" to be a 32-byte hash for EXTERNAL identifiers',
|
|
944
|
-
import_common7.INVALID_DID,
|
|
945
|
-
{ genesisBytes }
|
|
946
|
-
);
|
|
947
|
-
}
|
|
948
|
-
const hrp = idType === "KEY" ? "k" : "x";
|
|
949
|
-
const firstByte = version - 1 << 4 | networkValue;
|
|
950
|
-
const dataBytes = new Uint8Array([firstByte, ...genesisBytes]);
|
|
951
|
-
return `did:btcr2:${import_base.bech32m.encodeFromBytes(hrp, dataBytes)}`;
|
|
911
|
+
var import_utils3 = require("@web5/dids/utils");
|
|
912
|
+
var import_btc_signer2 = require("@scure/btc-signer");
|
|
913
|
+
var BTCR2_DID_DOCUMENT_CONTEXT = [
|
|
914
|
+
"https://www.w3.org/ns/did/v1.1",
|
|
915
|
+
"https://btcr2.dev/context/v1"
|
|
916
|
+
];
|
|
917
|
+
var MULTIKEY_VERIFICATION_METHOD_TYPE = "Multikey";
|
|
918
|
+
var MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX = "zQ3s";
|
|
919
|
+
var ID_PLACEHOLDER_VALUE = "did:btcr2:_";
|
|
920
|
+
var BECH32M_CHARS = "";
|
|
921
|
+
var DID_REGEX = /did:btcr2:(x1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]*)/g;
|
|
922
|
+
function isMultikeyVerificationMethod(vm) {
|
|
923
|
+
if (!Appendix.isDidVerificationMethod(vm)) {
|
|
924
|
+
return false;
|
|
952
925
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
if (
|
|
964
|
-
throw new import_common7.
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
}
|
|
970
|
-
if (method !== "btcr2") {
|
|
971
|
-
throw new import_common7.IdentifierError(`Invalid did method: ${method}`, import_common7.METHOD_NOT_SUPPORTED, { identifier });
|
|
926
|
+
const { type, publicKeyMultibase } = vm;
|
|
927
|
+
return type === MULTIKEY_VERIFICATION_METHOD_TYPE && typeof publicKeyMultibase === "string" && publicKeyMultibase.startsWith(MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX);
|
|
928
|
+
}
|
|
929
|
+
var DidVerificationMethod = class {
|
|
930
|
+
id;
|
|
931
|
+
type;
|
|
932
|
+
controller;
|
|
933
|
+
publicKeyMultibase;
|
|
934
|
+
secretKeyMultibase;
|
|
935
|
+
constructor({ id, type, controller, publicKeyMultibase, secretKeyMultibase }) {
|
|
936
|
+
if (type !== MULTIKEY_VERIFICATION_METHOD_TYPE) {
|
|
937
|
+
throw new import_common7.DidDocumentError(
|
|
938
|
+
`Invalid verification method: type must be "${MULTIKEY_VERIFICATION_METHOD_TYPE}"`,
|
|
939
|
+
import_common7.INVALID_DID_DOCUMENT,
|
|
940
|
+
{ id, type }
|
|
941
|
+
);
|
|
972
942
|
}
|
|
973
|
-
if (!
|
|
974
|
-
throw new import_common7.
|
|
943
|
+
if (typeof publicKeyMultibase !== "string" || !publicKeyMultibase.startsWith(MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX)) {
|
|
944
|
+
throw new import_common7.DidDocumentError(
|
|
945
|
+
`Invalid verification method: publicKeyMultibase must start with "${MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX}"`,
|
|
946
|
+
import_common7.INVALID_DID_DOCUMENT,
|
|
947
|
+
{ id, publicKeyMultibase }
|
|
948
|
+
);
|
|
975
949
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
950
|
+
this.id = id;
|
|
951
|
+
this.type = type;
|
|
952
|
+
this.controller = controller;
|
|
953
|
+
this.publicKeyMultibase = publicKeyMultibase;
|
|
954
|
+
this.secretKeyMultibase = secretKeyMultibase;
|
|
955
|
+
if (!secretKeyMultibase) {
|
|
956
|
+
delete this.secretKeyMultibase;
|
|
979
957
|
}
|
|
980
|
-
|
|
981
|
-
|
|
958
|
+
}
|
|
959
|
+
// TODO: Add helper methods and properties
|
|
960
|
+
};
|
|
961
|
+
var DidDocument = class _DidDocument {
|
|
962
|
+
id;
|
|
963
|
+
"@context" = [
|
|
964
|
+
"https://www.w3.org/ns/did/v1.1",
|
|
965
|
+
"https://btcr2.dev/context/v1"
|
|
966
|
+
];
|
|
967
|
+
verificationMethod;
|
|
968
|
+
authentication;
|
|
969
|
+
assertionMethod;
|
|
970
|
+
capabilityInvocation;
|
|
971
|
+
capabilityDelegation;
|
|
972
|
+
service;
|
|
973
|
+
deactivated;
|
|
974
|
+
constructor(document) {
|
|
975
|
+
if (!document.id) {
|
|
976
|
+
throw new import_common7.DidDocumentError("DID Document must have an id", import_common7.INVALID_DID_DOCUMENT, document);
|
|
982
977
|
}
|
|
983
|
-
const idType =
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
|
|
978
|
+
const idType = document.id.includes("k1") ? import_common7.IdentifierTypes.KEY : import_common7.IdentifierTypes.EXTERNAL;
|
|
979
|
+
const isGenesis = document.id === ID_PLACEHOLDER_VALUE;
|
|
980
|
+
const { id, verificationMethod: vm, service } = document;
|
|
981
|
+
if (!isGenesis) {
|
|
982
|
+
if (!_DidDocument.isValidId(id)) {
|
|
983
|
+
throw new import_common7.DidDocumentError(`Invalid id: ${id}`, import_common7.INVALID_DID_DOCUMENT, document);
|
|
984
|
+
}
|
|
985
|
+
if (!_DidDocument.isValidVerificationMethods(vm)) {
|
|
986
|
+
throw new import_common7.DidDocumentError("Invalid verificationMethod: " + vm, import_common7.INVALID_DID_DOCUMENT, document);
|
|
987
|
+
}
|
|
988
|
+
if (!_DidDocument.isValidServices(service)) {
|
|
989
|
+
throw new import_common7.DidDocumentError("Invalid service: " + service, import_common7.INVALID_DID_DOCUMENT, document);
|
|
990
|
+
}
|
|
987
991
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
992
|
+
this.id = document.id;
|
|
993
|
+
this.verificationMethod = document.verificationMethod || [];
|
|
994
|
+
this.service = document.service || [];
|
|
995
|
+
this["@context"] = document["@context"] || [
|
|
996
|
+
"https://www.w3.org/ns/did/v1.1",
|
|
997
|
+
"https://btcr2.dev/context/v1"
|
|
998
|
+
];
|
|
999
|
+
if (idType === import_common7.IdentifierTypes.KEY) {
|
|
1000
|
+
const keyRef = `${this.id}#initialKey`;
|
|
1001
|
+
this.authentication = document.authentication || [keyRef];
|
|
1002
|
+
this.assertionMethod = document.assertionMethod || [keyRef];
|
|
1003
|
+
this.capabilityInvocation = document.capabilityInvocation || [keyRef];
|
|
1004
|
+
this.capabilityDelegation = document.capabilityDelegation || [keyRef];
|
|
996
1005
|
} else {
|
|
997
|
-
|
|
1006
|
+
this.authentication = document.authentication;
|
|
1007
|
+
this.assertionMethod = document.assertionMethod;
|
|
1008
|
+
this.capabilityInvocation = document.capabilityInvocation;
|
|
1009
|
+
this.capabilityDelegation = document.capabilityDelegation;
|
|
998
1010
|
}
|
|
999
|
-
|
|
1000
|
-
if (
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
throw new import_common7.IdentifierError(`Invalid genesisBytes: ${genesisBytes}`, import_common7.INVALID_DID, { identifier });
|
|
1005
|
-
}
|
|
1006
|
-
} else if (genesisBytes.length !== 32) {
|
|
1007
|
-
throw new import_common7.IdentifierError(`Invalid genesisBytes: ${genesisBytes}`, import_common7.INVALID_DID, { identifier });
|
|
1011
|
+
_DidDocument.sanitize(this);
|
|
1012
|
+
if (isGenesis) {
|
|
1013
|
+
this.validateGenesis();
|
|
1014
|
+
} else {
|
|
1015
|
+
_DidDocument.validate(this);
|
|
1008
1016
|
}
|
|
1009
|
-
return { idType, hrp, version, network, genesisBytes };
|
|
1010
1017
|
}
|
|
1011
1018
|
/**
|
|
1012
|
-
*
|
|
1013
|
-
* @returns {
|
|
1019
|
+
* Convert the DidDocument to a JSON object.
|
|
1020
|
+
* @returns {DidDocument} The JSON representation of the DidDocument.
|
|
1014
1021
|
*/
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1022
|
+
toJSON() {
|
|
1023
|
+
return {
|
|
1024
|
+
id: this.id,
|
|
1025
|
+
"@context": this["@context"],
|
|
1026
|
+
verificationMethod: this.verificationMethod,
|
|
1027
|
+
authentication: this.authentication,
|
|
1028
|
+
assertionMethod: this.assertionMethod,
|
|
1029
|
+
capabilityInvocation: this.capabilityInvocation,
|
|
1030
|
+
capabilityDelegation: this.capabilityDelegation,
|
|
1031
|
+
service: this.service,
|
|
1032
|
+
deactivated: this.deactivated
|
|
1033
|
+
};
|
|
1026
1034
|
}
|
|
1027
1035
|
/**
|
|
1028
|
-
*
|
|
1029
|
-
* @param {string}
|
|
1030
|
-
* @
|
|
1031
|
-
* @
|
|
1036
|
+
* Create a minimal DidDocument from "k1" btcr2 identifier.
|
|
1037
|
+
* @param {string} publicKeyMultibase The public key in multibase format.
|
|
1038
|
+
* @param {Array<BeaconService>} service The beacon services to be included in the document.
|
|
1039
|
+
* @returns {DidDocument} A new DidDocument with the placeholder ID.
|
|
1032
1040
|
*/
|
|
1033
|
-
static
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1041
|
+
static fromKeyIdentifier(id, publicKeyMultibase, service) {
|
|
1042
|
+
id = id.includes("#") ? id : `${id}#initialKey`;
|
|
1043
|
+
const document = {
|
|
1044
|
+
id,
|
|
1045
|
+
verificationMethod: [
|
|
1046
|
+
new DidVerificationMethod({
|
|
1047
|
+
id,
|
|
1048
|
+
type: "Multikey",
|
|
1049
|
+
controller: id,
|
|
1050
|
+
publicKeyMultibase
|
|
1051
|
+
})
|
|
1052
|
+
],
|
|
1053
|
+
service
|
|
1054
|
+
};
|
|
1055
|
+
return new _DidDocument(document);
|
|
1043
1056
|
}
|
|
1044
1057
|
/**
|
|
1045
|
-
*
|
|
1046
|
-
* @param {
|
|
1047
|
-
* @returns {
|
|
1058
|
+
* Create a DidDocument from "x1" btcr2 identifier.
|
|
1059
|
+
* @param {ExternalData} data The verification methods of the DID Document.
|
|
1060
|
+
* @returns {DidDocument} A new DidDocument.
|
|
1048
1061
|
*/
|
|
1049
|
-
static
|
|
1050
|
-
|
|
1051
|
-
this.decode(identifier);
|
|
1052
|
-
return true;
|
|
1053
|
-
} catch {
|
|
1054
|
-
return false;
|
|
1055
|
-
}
|
|
1062
|
+
static fromExternalIdentifier(data) {
|
|
1063
|
+
return new _DidDocument(data);
|
|
1056
1064
|
}
|
|
1057
|
-
};
|
|
1058
|
-
|
|
1059
|
-
// src/core/beacon/utils.ts
|
|
1060
|
-
var BeaconUtils = class {
|
|
1061
1065
|
/**
|
|
1062
|
-
*
|
|
1063
|
-
* @
|
|
1064
|
-
* @returns {string} The Bitcoin address extracted from the URI
|
|
1065
|
-
* @throws {DidMethodError} if the URI is not a valid Bitcoin URI
|
|
1066
|
+
* Sanitize the DID Document by removing undefined values
|
|
1067
|
+
* @returns {DidDocument} The sanitized DID Document
|
|
1066
1068
|
*/
|
|
1067
|
-
static
|
|
1068
|
-
|
|
1069
|
-
|
|
1069
|
+
static sanitize(doc) {
|
|
1070
|
+
for (const key of Object.keys(doc)) {
|
|
1071
|
+
if (doc[key] === void 0) {
|
|
1072
|
+
delete doc[key];
|
|
1073
|
+
}
|
|
1070
1074
|
}
|
|
1071
|
-
return
|
|
1075
|
+
return doc;
|
|
1072
1076
|
}
|
|
1073
1077
|
/**
|
|
1074
|
-
* Validates
|
|
1075
|
-
* @param {
|
|
1076
|
-
* @returns {boolean}
|
|
1078
|
+
* Validates a DidDocument by breaking it into modular validation methods.
|
|
1079
|
+
* @param {DidDocument} didDocument The DID document to validate.
|
|
1080
|
+
* @returns {boolean} True if the DID document is valid.
|
|
1081
|
+
* @throws {DidDocumentError} If any validation check fails.
|
|
1077
1082
|
*/
|
|
1078
|
-
static
|
|
1079
|
-
if (!
|
|
1080
|
-
|
|
1081
|
-
|
|
1083
|
+
static isValid(didDocument) {
|
|
1084
|
+
if (!this.isValidContext(didDocument?.["@context"])) {
|
|
1085
|
+
throw new import_common7.DidDocumentError('Invalid "@context"', import_common7.INVALID_DID_DOCUMENT, didDocument);
|
|
1086
|
+
}
|
|
1087
|
+
if (!this.isValidId(didDocument?.id)) {
|
|
1088
|
+
throw new import_common7.DidDocumentError('Invalid "id"', import_common7.INVALID_DID_DOCUMENT, didDocument);
|
|
1089
|
+
}
|
|
1090
|
+
if (!this.isValidVerificationMethods(didDocument?.verificationMethod)) {
|
|
1091
|
+
throw new import_common7.DidDocumentError('Invalid "verificationMethod"', import_common7.INVALID_DID_DOCUMENT, didDocument);
|
|
1092
|
+
}
|
|
1093
|
+
if (!this.isValidServices(didDocument?.service)) {
|
|
1094
|
+
throw new import_common7.DidDocumentError('Invalid "service"', import_common7.INVALID_DID_DOCUMENT, didDocument);
|
|
1095
|
+
}
|
|
1096
|
+
if (!this.isValidVerificationRelationships(didDocument)) {
|
|
1097
|
+
throw new import_common7.DidDocumentError("Invalid verification relationships", import_common7.INVALID_DID_DOCUMENT, didDocument);
|
|
1098
|
+
}
|
|
1082
1099
|
return true;
|
|
1083
1100
|
}
|
|
1084
1101
|
/**
|
|
1085
|
-
*
|
|
1086
|
-
* @
|
|
1087
|
-
* @
|
|
1088
|
-
* @
|
|
1102
|
+
* Validates that "@context" exists and includes correct values.
|
|
1103
|
+
* @private
|
|
1104
|
+
* @param {DidDocument['@context']} context The context to validate.
|
|
1105
|
+
* @returns {boolean} True if the context is valid.
|
|
1089
1106
|
*/
|
|
1090
|
-
static
|
|
1091
|
-
|
|
1107
|
+
static isValidContext(context) {
|
|
1108
|
+
if (!Array.isArray(context) || context.length === 0) return false;
|
|
1109
|
+
return BTCR2_DID_DOCUMENT_CONTEXT.every((required) => context.includes(required));
|
|
1092
1110
|
}
|
|
1093
1111
|
/**
|
|
1094
|
-
*
|
|
1095
|
-
* @
|
|
1096
|
-
* @
|
|
1097
|
-
* @
|
|
1112
|
+
* Validates that the DID Document has a valid id.
|
|
1113
|
+
* @private
|
|
1114
|
+
* @param {string} id The id to validate.
|
|
1115
|
+
* @returns {boolean} True if the id is valid.
|
|
1098
1116
|
*/
|
|
1099
|
-
static
|
|
1117
|
+
static isValidId(id) {
|
|
1118
|
+
if (typeof id !== "string") return false;
|
|
1100
1119
|
try {
|
|
1101
|
-
|
|
1102
|
-
return
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
} catch (error) {
|
|
1106
|
-
throw new BeaconError(
|
|
1107
|
-
"Failed to create beacon services: " + error.message,
|
|
1108
|
-
"BEACON_SERVICE_ERROR",
|
|
1109
|
-
{ did, beaconType }
|
|
1110
|
-
);
|
|
1120
|
+
Identifier.decode(id);
|
|
1121
|
+
return true;
|
|
1122
|
+
} catch {
|
|
1123
|
+
return false;
|
|
1111
1124
|
}
|
|
1112
1125
|
}
|
|
1113
1126
|
/**
|
|
1114
|
-
*
|
|
1115
|
-
* @
|
|
1116
|
-
* @param {
|
|
1117
|
-
* @
|
|
1118
|
-
* @returns {BeaconService} A BeaconService object.
|
|
1119
|
-
* @throws {DidMethodError} if the bitcoin address is invalid.
|
|
1127
|
+
* Validates that verification methods exist and are correctly formatted.
|
|
1128
|
+
* @private
|
|
1129
|
+
* @param {DidVerificationMethod[]} verificationMethod The verification methods to validate.
|
|
1130
|
+
* @returns {boolean} True if the verification methods are valid.
|
|
1120
1131
|
*/
|
|
1121
|
-
static
|
|
1122
|
-
|
|
1123
|
-
const components = Identifier.decode(did);
|
|
1124
|
-
const network = (0, import_bitcoin2.getNetwork)(components.network);
|
|
1125
|
-
const pubkey = components.genesisBytes;
|
|
1126
|
-
const id = `${did}#initial${addressType.toUpperCase()}`;
|
|
1127
|
-
const address = addressType === "p2tr" ? (0, import_btc_signer2.p2tr)(pubkey.slice(1, 33), void 0, network).address : addressType === "p2wpkh" ? (0, import_btc_signer2.p2wpkh)(pubkey, network).address : (0, import_btc_signer2.p2pkh)(pubkey, network).address;
|
|
1128
|
-
const serviceEndpoint = `bitcoin:${address}`;
|
|
1129
|
-
return { id, type: beaconType, serviceEndpoint };
|
|
1130
|
-
} catch (error) {
|
|
1131
|
-
throw new BeaconError(
|
|
1132
|
-
"Failed to create beacon service: " + error.message,
|
|
1133
|
-
"BEACON_SERVICE_ERROR",
|
|
1134
|
-
{ did, beaconType }
|
|
1135
|
-
);
|
|
1136
|
-
}
|
|
1132
|
+
static isValidVerificationMethods(verificationMethod) {
|
|
1133
|
+
return Array.isArray(verificationMethod) && verificationMethod.every(isMultikeyVerificationMethod);
|
|
1137
1134
|
}
|
|
1138
1135
|
/**
|
|
1139
|
-
*
|
|
1140
|
-
* @
|
|
1141
|
-
* @
|
|
1142
|
-
* @
|
|
1136
|
+
* Validates that the DID Document has valid services.
|
|
1137
|
+
* @private
|
|
1138
|
+
* @param {DidService[]} service The services to validate.
|
|
1139
|
+
* @returns {boolean} True if the services are valid.
|
|
1143
1140
|
*/
|
|
1144
|
-
static
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
serviceEndpoint: `bitcoin:${p2trAddr}`
|
|
1167
|
-
}
|
|
1168
|
-
];
|
|
1169
|
-
} catch (error) {
|
|
1170
|
-
throw new BeaconError(
|
|
1171
|
-
"Failed to create beacon services: " + error.message,
|
|
1172
|
-
"BEACON_SERVICE_ERROR",
|
|
1173
|
-
{ id, publicKey, network, beaconType }
|
|
1141
|
+
static isValidServices(service) {
|
|
1142
|
+
return Array.isArray(service) && service.every(import_utils3.isDidService);
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Validates verification relationships (authentication, assertionMethod, capabilityInvocation, capabilityDelegation).
|
|
1146
|
+
* @private
|
|
1147
|
+
* @param {DidDocument} didDocument The DID Document to validate.
|
|
1148
|
+
* @returns {boolean} True if the verification relationships are valid.
|
|
1149
|
+
*/
|
|
1150
|
+
static isValidVerificationRelationships(didDocument) {
|
|
1151
|
+
const possibleVerificationRelationships = [
|
|
1152
|
+
"authentication",
|
|
1153
|
+
"assertionMethod",
|
|
1154
|
+
"capabilityInvocation",
|
|
1155
|
+
"capabilityDelegation"
|
|
1156
|
+
];
|
|
1157
|
+
const keys = Object.keys(didDocument);
|
|
1158
|
+
const availableKeys = possibleVerificationRelationships.filter((key) => keys.includes(key));
|
|
1159
|
+
return availableKeys.every((key) => {
|
|
1160
|
+
const value = didDocument[key];
|
|
1161
|
+
return value && Array.isArray(value) && value.every(
|
|
1162
|
+
(entry) => typeof entry === "string" || Appendix.isDidVerificationMethod(entry)
|
|
1174
1163
|
);
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Validate the DID Document
|
|
1168
|
+
* @returns {DidDocument} Validated DID Document.
|
|
1169
|
+
* @throws {DidDocumentError} If the DID Document is invalid.
|
|
1170
|
+
*/
|
|
1171
|
+
static validate(didDocument) {
|
|
1172
|
+
if (didDocument.id === ID_PLACEHOLDER_VALUE) {
|
|
1173
|
+
didDocument.validateGenesis();
|
|
1174
|
+
} else {
|
|
1175
|
+
_DidDocument.isValid(didDocument);
|
|
1175
1176
|
}
|
|
1177
|
+
return didDocument;
|
|
1176
1178
|
}
|
|
1177
1179
|
/**
|
|
1178
|
-
*
|
|
1179
|
-
* @
|
|
1180
|
-
* @returns {BeaconServiceAddress} The beacon service with the address field extracted from the serviceEndpoint.
|
|
1180
|
+
* Validate the GenesisDocument.
|
|
1181
|
+
* @returns {boolean} True if the GenesisDocument is valid.
|
|
1181
1182
|
*/
|
|
1182
|
-
|
|
1183
|
-
|
|
1183
|
+
validateGenesis() {
|
|
1184
|
+
if (this.id !== ID_PLACEHOLDER_VALUE) {
|
|
1185
|
+
throw new import_common7.DidDocumentError("Invalid GenesisDocument ID", import_common7.INVALID_DID_DOCUMENT, this);
|
|
1186
|
+
}
|
|
1187
|
+
if (!this.verificationMethod.every((vm) => vm.id.includes(ID_PLACEHOLDER_VALUE) && vm.controller === ID_PLACEHOLDER_VALUE)) {
|
|
1188
|
+
throw new import_common7.DidDocumentError("Invalid GenesisDocument verificationMethod", import_common7.INVALID_DID_DOCUMENT, this);
|
|
1189
|
+
}
|
|
1190
|
+
if (!this.service.every((svc) => svc.id.includes(ID_PLACEHOLDER_VALUE))) {
|
|
1191
|
+
throw new import_common7.DidDocumentError("Invalid GenesisDocument service", import_common7.INVALID_DID_DOCUMENT, this);
|
|
1192
|
+
}
|
|
1193
|
+
if (!_DidDocument.isValidVerificationRelationships(this)) {
|
|
1194
|
+
throw new import_common7.DidDocumentError("Invalid GenesisDocument assertionMethod", import_common7.INVALID_DID_DOCUMENT, this);
|
|
1195
|
+
}
|
|
1196
|
+
return true;
|
|
1184
1197
|
}
|
|
1185
1198
|
/**
|
|
1186
|
-
*
|
|
1187
|
-
* @
|
|
1188
|
-
* @returns {string[]} An array of beacon service ids.
|
|
1199
|
+
* Convert the DidDocument to an GenesisDocument.
|
|
1200
|
+
* @returns {GenesisDocument} The GenesisDocument representation of the DidDocument.
|
|
1189
1201
|
*/
|
|
1190
|
-
|
|
1191
|
-
|
|
1202
|
+
toIntermediate() {
|
|
1203
|
+
if (this.id.includes("k1")) {
|
|
1204
|
+
throw new import_common7.DidDocumentError("Cannot convert a key identifier to an intermediate document", import_common7.INVALID_DID_DOCUMENT, this);
|
|
1205
|
+
}
|
|
1206
|
+
return new GenesisDocument(this);
|
|
1192
1207
|
}
|
|
1193
1208
|
};
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
function extractOpReturnSignalHash(scriptPubKey) {
|
|
1198
|
-
if (!scriptPubKey) {
|
|
1199
|
-
return null;
|
|
1200
|
-
}
|
|
1201
|
-
const signal = BEACON_SIGNAL_SCRIPT.exec(scriptPubKey.trim());
|
|
1202
|
-
if (!signal) {
|
|
1203
|
-
return null;
|
|
1209
|
+
var GenesisDocument = class _GenesisDocument extends DidDocument {
|
|
1210
|
+
constructor(document) {
|
|
1211
|
+
super(document);
|
|
1204
1212
|
}
|
|
1205
|
-
return signal[1].toLowerCase();
|
|
1206
|
-
}
|
|
1207
|
-
var BeaconSignalDiscovery = class _BeaconSignalDiscovery {
|
|
1208
1213
|
/**
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1211
|
-
*
|
|
1212
|
-
* A Beacon Signal is a transaction that *spends from* a Beacon Address, but an address
|
|
1213
|
-
* transaction listing returns every transaction touching the address in either
|
|
1214
|
-
* direction. Without this check, anyone able to pay dust to a beacon address could
|
|
1215
|
-
* attach an arbitrary 32-byte OP_RETURN and have it read as a signal, so the input side
|
|
1216
|
-
* has to be inspected before a transaction is treated as one.
|
|
1217
|
-
*
|
|
1218
|
-
* Esplora embeds the spent output in `vin[].prevout`; when a backend omits it the
|
|
1219
|
-
* funding transaction is fetched instead, so a missing field cannot silently drop a
|
|
1220
|
-
* real signal.
|
|
1221
|
-
*
|
|
1222
|
-
* @param {RawTransactionRest} tx The candidate transaction.
|
|
1223
|
-
* @param {string} address The beacon address the transaction must spend from.
|
|
1224
|
-
* @param {BitcoinConnection} bitcoin Bitcoin network connection to use for REST calls.
|
|
1225
|
-
* @returns {Promise<boolean>} True if at least one input spends an output of the beacon address.
|
|
1214
|
+
* Convert the GenesisDocument to a DidDocument by replacing the placeholder value with the provided DID.
|
|
1215
|
+
* @param did The DID to replace the placeholder value in the document.
|
|
1216
|
+
* @returns {DidDocument} A new DidDocument with the placeholder value replaced by the provided DID.
|
|
1226
1217
|
*/
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
}
|
|
1232
|
-
let prevout = vin.prevout;
|
|
1233
|
-
if (!prevout && vin.txid) {
|
|
1234
|
-
const fundingTx = await bitcoin.rest.transaction.get(vin.txid);
|
|
1235
|
-
prevout = fundingTx?.vout?.[vin.vout];
|
|
1236
|
-
}
|
|
1237
|
-
if (prevout?.scriptpubkey_address === address) {
|
|
1238
|
-
return true;
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
return false;
|
|
1218
|
+
toDidDocument(did) {
|
|
1219
|
+
const stringThis = JSON.stringify(this).replaceAll(ID_PLACEHOLDER_VALUE, did);
|
|
1220
|
+
const parseThis = JSON.parse(stringThis);
|
|
1221
|
+
return new DidDocument(parseThis);
|
|
1242
1222
|
}
|
|
1243
1223
|
/**
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
* @
|
|
1247
|
-
* @param {BitcoinConnection} bitcoin Bitcoin network connection to use for REST calls
|
|
1248
|
-
* @returns {Promise<Map<BeaconService, Array<BeaconSignal>>>} Map of beacon service to its discovered signals
|
|
1224
|
+
* Create an GenesisDocument from a DidDocument by replacing the DID with a placeholder value.
|
|
1225
|
+
* @param {DidDocument} didDocument The DidDocument to convert.
|
|
1226
|
+
* @returns {GenesisDocument} The GenesisDocument representation of the DidDocument.
|
|
1249
1227
|
*/
|
|
1250
|
-
static
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
for (const beaconService of beaconServices) {
|
|
1254
|
-
beaconServiceSignals.set(beaconService, []);
|
|
1255
|
-
const beaconAddress = BeaconUtils.parseBitcoinAddress(beaconService.serviceEndpoint);
|
|
1256
|
-
const beaconSignals = await bitcoin.rest.address.getTxs(beaconAddress);
|
|
1257
|
-
if (!beaconSignals || !beaconSignals.length) {
|
|
1258
|
-
continue;
|
|
1259
|
-
}
|
|
1260
|
-
for (const beaconSignal of beaconSignals) {
|
|
1261
|
-
const lastSignalVout = beaconSignal.vout.slice(-1)[0];
|
|
1262
|
-
if (!lastSignalVout) {
|
|
1263
|
-
continue;
|
|
1264
|
-
}
|
|
1265
|
-
const updateHash = extractOpReturnSignalHash(lastSignalVout.scriptpubkey);
|
|
1266
|
-
if (!updateHash) {
|
|
1267
|
-
continue;
|
|
1268
|
-
}
|
|
1269
|
-
if (!await _BeaconSignalDiscovery.spendsFromAddress(beaconSignal, beaconAddress, bitcoin)) {
|
|
1270
|
-
continue;
|
|
1271
|
-
}
|
|
1272
|
-
const confirmations = currentBlockCount - beaconSignal.status.block_height + 1;
|
|
1273
|
-
beaconServiceSignals.get(beaconService)?.push({
|
|
1274
|
-
tx: beaconSignal,
|
|
1275
|
-
signalBytes: updateHash,
|
|
1276
|
-
blockMetadata: {
|
|
1277
|
-
confirmations,
|
|
1278
|
-
height: beaconSignal.status.block_height,
|
|
1279
|
-
time: beaconSignal.status.block_time
|
|
1280
|
-
}
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
return beaconServiceSignals;
|
|
1228
|
+
static fromDidDocument(didDocument) {
|
|
1229
|
+
const intermediateDocument = import_common7.JSONUtils.cloneReplace(didDocument, DID_REGEX, ID_PLACEHOLDER_VALUE);
|
|
1230
|
+
return new _GenesisDocument(intermediateDocument);
|
|
1285
1231
|
}
|
|
1286
1232
|
/**
|
|
1287
|
-
*
|
|
1288
|
-
* @param {Array<
|
|
1289
|
-
* @param {
|
|
1290
|
-
* @
|
|
1233
|
+
* Create a minimal GenesisDocument with a placeholder ID.
|
|
1234
|
+
* @param {Array<DidVerificationMethod>} verificationMethod The public key in multibase format.
|
|
1235
|
+
* @param {VerificationRelationships} relationships The public key in multibase format.
|
|
1236
|
+
* @param {Array<BeaconService>} service The service to be included in the document.
|
|
1237
|
+
* @returns {GenesisDocument} A new GenesisDocument with the placeholder ID.
|
|
1291
1238
|
*/
|
|
1292
|
-
static
|
|
1293
|
-
|
|
1294
|
-
for (const beaconService of beaconServices) {
|
|
1295
|
-
beaconServiceSignals.set(beaconService, []);
|
|
1296
|
-
}
|
|
1297
|
-
const rpc = bitcoin.rpc;
|
|
1298
|
-
if (!rpc) {
|
|
1299
|
-
throw new import_common9.ResolveError("RPC connection is not available", "RPC_CONNECTION_ERROR", bitcoin);
|
|
1300
|
-
}
|
|
1301
|
-
const targetHeight = await rpc.getBlockCount();
|
|
1302
|
-
const beaconServicesMap = new Map(
|
|
1303
|
-
beaconServices.map((service) => [BeaconUtils.parseBitcoinAddress(service.serviceEndpoint), service])
|
|
1304
|
-
);
|
|
1305
|
-
let height = 0;
|
|
1306
|
-
let block = await bitcoin.rpc.getBlock({ height });
|
|
1307
|
-
console.info(`Searching for beacon signals, please wait ...`);
|
|
1308
|
-
while (block.height <= targetHeight) {
|
|
1309
|
-
for (const tx of block.tx) {
|
|
1310
|
-
if (tx.txid === import_bitcoin3.GENESIS_TX_ID) {
|
|
1311
|
-
continue;
|
|
1312
|
-
}
|
|
1313
|
-
const lastSignalVout = tx.vout.slice(-1)[0];
|
|
1314
|
-
if (!lastSignalVout) {
|
|
1315
|
-
continue;
|
|
1316
|
-
}
|
|
1317
|
-
const updateHash = extractOpReturnSignalHash(lastSignalVout.scriptPubKey?.hex);
|
|
1318
|
-
if (!updateHash) {
|
|
1319
|
-
continue;
|
|
1320
|
-
}
|
|
1321
|
-
const signaled = /* @__PURE__ */ new Set();
|
|
1322
|
-
for (const vin of tx.vin) {
|
|
1323
|
-
if (vin.coinbase) {
|
|
1324
|
-
continue;
|
|
1325
|
-
}
|
|
1326
|
-
if (vin.txinwitness && vin.txinwitness.length === 1 && vin.txinwitness[0] === import_bitcoin3.TXIN_WITNESS_COINBASE) {
|
|
1327
|
-
continue;
|
|
1328
|
-
}
|
|
1329
|
-
if (!vin.txid) {
|
|
1330
|
-
continue;
|
|
1331
|
-
}
|
|
1332
|
-
if (vin.vout === void 0) {
|
|
1333
|
-
continue;
|
|
1334
|
-
}
|
|
1335
|
-
const prevout = await rpc.getRawTransaction(vin.txid, 2);
|
|
1336
|
-
if (!prevout.vout[vin.vout]) {
|
|
1337
|
-
continue;
|
|
1338
|
-
}
|
|
1339
|
-
const scriptPubKey = prevout.vout[vin.vout].scriptPubKey;
|
|
1340
|
-
if (!scriptPubKey.address) {
|
|
1341
|
-
continue;
|
|
1342
|
-
}
|
|
1343
|
-
const beaconService = beaconServicesMap.get(scriptPubKey.address);
|
|
1344
|
-
if (!beaconService || signaled.has(beaconService)) {
|
|
1345
|
-
continue;
|
|
1346
|
-
}
|
|
1347
|
-
signaled.add(beaconService);
|
|
1348
|
-
console.info(`Tx ${tx.txid} contains beacon address ${scriptPubKey.address}`);
|
|
1349
|
-
beaconServiceSignals.get(beaconService)?.push({
|
|
1350
|
-
tx,
|
|
1351
|
-
signalBytes: updateHash,
|
|
1352
|
-
blockMetadata: {
|
|
1353
|
-
height: block.height,
|
|
1354
|
-
time: block.time,
|
|
1355
|
-
confirmations: block.confirmations
|
|
1356
|
-
}
|
|
1357
|
-
});
|
|
1358
|
-
}
|
|
1359
|
-
;
|
|
1360
|
-
}
|
|
1361
|
-
height += 1;
|
|
1362
|
-
if (height > targetHeight) {
|
|
1363
|
-
console.info(`Chain tip reached ${height}, breaking ...`);
|
|
1364
|
-
break;
|
|
1365
|
-
}
|
|
1366
|
-
block = await rpc.getBlock({ height });
|
|
1367
|
-
}
|
|
1368
|
-
return beaconServiceSignals;
|
|
1369
|
-
}
|
|
1370
|
-
};
|
|
1371
|
-
|
|
1372
|
-
// src/core/did-sender-resolver.ts
|
|
1373
|
-
var import_common14 = require("@did-btcr2/common");
|
|
1374
|
-
var import_cryptosuite3 = require("@did-btcr2/cryptosuite");
|
|
1375
|
-
var import_keypair4 = require("@did-btcr2/keypair");
|
|
1376
|
-
|
|
1377
|
-
// src/core/resolver.ts
|
|
1378
|
-
var import_bitcoin5 = require("@did-btcr2/bitcoin");
|
|
1379
|
-
var import_common13 = require("@did-btcr2/common");
|
|
1380
|
-
var import_cryptosuite2 = require("@did-btcr2/cryptosuite");
|
|
1381
|
-
var import_keypair3 = require("@did-btcr2/keypair");
|
|
1382
|
-
|
|
1383
|
-
// src/did-btcr2.ts
|
|
1384
|
-
var import_common12 = require("@did-btcr2/common");
|
|
1385
|
-
var import_dids2 = require("@web5/dids");
|
|
1386
|
-
|
|
1387
|
-
// src/core/updater.ts
|
|
1388
|
-
var import_common11 = require("@did-btcr2/common");
|
|
1389
|
-
var import_cryptosuite = require("@did-btcr2/cryptosuite");
|
|
1390
|
-
|
|
1391
|
-
// src/utils/did-document.ts
|
|
1392
|
-
var import_bitcoin4 = require("@did-btcr2/bitcoin");
|
|
1393
|
-
var import_common10 = require("@did-btcr2/common");
|
|
1394
|
-
var import_keypair2 = require("@did-btcr2/keypair");
|
|
1395
|
-
var import_utils4 = require("@web5/dids/utils");
|
|
1396
|
-
var import_btc_signer3 = require("@scure/btc-signer");
|
|
1397
|
-
var BTCR2_DID_DOCUMENT_CONTEXT = [
|
|
1398
|
-
"https://www.w3.org/ns/did/v1.1",
|
|
1399
|
-
"https://btcr2.dev/context/v1"
|
|
1400
|
-
];
|
|
1401
|
-
var MULTIKEY_VERIFICATION_METHOD_TYPE = "Multikey";
|
|
1402
|
-
var MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX = "zQ3s";
|
|
1403
|
-
var ID_PLACEHOLDER_VALUE = "did:btcr2:_";
|
|
1404
|
-
var BECH32M_CHARS = "";
|
|
1405
|
-
var DID_REGEX = /did:btcr2:(x1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]*)/g;
|
|
1406
|
-
function isMultikeyVerificationMethod(vm) {
|
|
1407
|
-
if (!Appendix.isDidVerificationMethod(vm)) {
|
|
1408
|
-
return false;
|
|
1409
|
-
}
|
|
1410
|
-
const { type, publicKeyMultibase } = vm;
|
|
1411
|
-
return type === MULTIKEY_VERIFICATION_METHOD_TYPE && typeof publicKeyMultibase === "string" && publicKeyMultibase.startsWith(MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX);
|
|
1412
|
-
}
|
|
1413
|
-
var DidVerificationMethod = class {
|
|
1414
|
-
id;
|
|
1415
|
-
type;
|
|
1416
|
-
controller;
|
|
1417
|
-
publicKeyMultibase;
|
|
1418
|
-
secretKeyMultibase;
|
|
1419
|
-
constructor({ id, type, controller, publicKeyMultibase, secretKeyMultibase }) {
|
|
1420
|
-
if (type !== MULTIKEY_VERIFICATION_METHOD_TYPE) {
|
|
1421
|
-
throw new import_common10.DidDocumentError(
|
|
1422
|
-
`Invalid verification method: type must be "${MULTIKEY_VERIFICATION_METHOD_TYPE}"`,
|
|
1423
|
-
import_common10.INVALID_DID_DOCUMENT,
|
|
1424
|
-
{ id, type }
|
|
1425
|
-
);
|
|
1426
|
-
}
|
|
1427
|
-
if (typeof publicKeyMultibase !== "string" || !publicKeyMultibase.startsWith(MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX)) {
|
|
1428
|
-
throw new import_common10.DidDocumentError(
|
|
1429
|
-
`Invalid verification method: publicKeyMultibase must start with "${MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX}"`,
|
|
1430
|
-
import_common10.INVALID_DID_DOCUMENT,
|
|
1431
|
-
{ id, publicKeyMultibase }
|
|
1432
|
-
);
|
|
1433
|
-
}
|
|
1434
|
-
this.id = id;
|
|
1435
|
-
this.type = type;
|
|
1436
|
-
this.controller = controller;
|
|
1437
|
-
this.publicKeyMultibase = publicKeyMultibase;
|
|
1438
|
-
this.secretKeyMultibase = secretKeyMultibase;
|
|
1439
|
-
if (!secretKeyMultibase) {
|
|
1440
|
-
delete this.secretKeyMultibase;
|
|
1441
|
-
}
|
|
1239
|
+
static create(verificationMethod, relationships, service) {
|
|
1240
|
+
return new _GenesisDocument({ id: ID_PLACEHOLDER_VALUE, ...relationships, verificationMethod, service });
|
|
1442
1241
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
if (!_DidDocument.isValidVerificationMethods(vm)) {
|
|
1470
|
-
throw new import_common10.DidDocumentError("Invalid verificationMethod: " + vm, import_common10.INVALID_DID_DOCUMENT, document);
|
|
1471
|
-
}
|
|
1472
|
-
if (!_DidDocument.isValidServices(service)) {
|
|
1473
|
-
throw new import_common10.DidDocumentError("Invalid service: " + service, import_common10.INVALID_DID_DOCUMENT, document);
|
|
1242
|
+
/**
|
|
1243
|
+
* Create a minimal GenesisDocument from a public key.
|
|
1244
|
+
* @param {KeyBytes} publicKey The public key in bytes format.
|
|
1245
|
+
* @returns {GenesisDocument} A new GenesisDocument with the placeholder ID.
|
|
1246
|
+
*/
|
|
1247
|
+
static fromPublicKey(publicKey, network) {
|
|
1248
|
+
const pk = new import_keypair.CompressedSecp256k1PublicKey(publicKey);
|
|
1249
|
+
const id = ID_PLACEHOLDER_VALUE;
|
|
1250
|
+
const address = (0, import_btc_signer2.p2pkh)(pk.compressed, (0, import_bitcoin2.getNetwork)(network)).address;
|
|
1251
|
+
const services = [{
|
|
1252
|
+
id: `${id}#service-0`,
|
|
1253
|
+
serviceEndpoint: `bitcoin:${address}`,
|
|
1254
|
+
type: "SingletonBeacon"
|
|
1255
|
+
}];
|
|
1256
|
+
const relationships = {
|
|
1257
|
+
authentication: [`${id}#key-0`],
|
|
1258
|
+
assertionMethod: [`${id}#key-0`],
|
|
1259
|
+
capabilityInvocation: [`${id}#key-0`],
|
|
1260
|
+
capabilityDelegation: [`${id}#key-0`]
|
|
1261
|
+
};
|
|
1262
|
+
const verificationMethod = [
|
|
1263
|
+
{
|
|
1264
|
+
id: `${id}#key-0`,
|
|
1265
|
+
type: "Multikey",
|
|
1266
|
+
controller: id,
|
|
1267
|
+
publicKeyMultibase: pk.multibase.encoded
|
|
1474
1268
|
}
|
|
1475
|
-
}
|
|
1476
|
-
this.id = document.id;
|
|
1477
|
-
this.verificationMethod = document.verificationMethod || [];
|
|
1478
|
-
this.service = document.service || [];
|
|
1479
|
-
this["@context"] = document["@context"] || [
|
|
1480
|
-
"https://www.w3.org/ns/did/v1.1",
|
|
1481
|
-
"https://btcr2.dev/context/v1"
|
|
1482
1269
|
];
|
|
1483
|
-
|
|
1484
|
-
const keyRef = `${this.id}#initialKey`;
|
|
1485
|
-
this.authentication = document.authentication || [keyRef];
|
|
1486
|
-
this.assertionMethod = document.assertionMethod || [keyRef];
|
|
1487
|
-
this.capabilityInvocation = document.capabilityInvocation || [keyRef];
|
|
1488
|
-
this.capabilityDelegation = document.capabilityDelegation || [keyRef];
|
|
1489
|
-
} else {
|
|
1490
|
-
this.authentication = document.authentication;
|
|
1491
|
-
this.assertionMethod = document.assertionMethod;
|
|
1492
|
-
this.capabilityInvocation = document.capabilityInvocation;
|
|
1493
|
-
this.capabilityDelegation = document.capabilityDelegation;
|
|
1494
|
-
}
|
|
1495
|
-
_DidDocument.sanitize(this);
|
|
1496
|
-
if (isGenesis) {
|
|
1497
|
-
this.validateGenesis();
|
|
1498
|
-
} else {
|
|
1499
|
-
_DidDocument.validate(this);
|
|
1500
|
-
}
|
|
1270
|
+
return _GenesisDocument.create(verificationMethod, relationships, services);
|
|
1501
1271
|
}
|
|
1502
1272
|
/**
|
|
1503
|
-
*
|
|
1504
|
-
* @
|
|
1273
|
+
* Taken an object, convert it to an IntermediateDocuemnt and then to a DidDocument.
|
|
1274
|
+
* @param {object | DidDocument} object The JSON object to convert.
|
|
1275
|
+
* @returns {DidDocument} The created DidDocument.
|
|
1505
1276
|
*/
|
|
1506
|
-
|
|
1507
|
-
return
|
|
1508
|
-
id: this.id,
|
|
1509
|
-
"@context": this["@context"],
|
|
1510
|
-
verificationMethod: this.verificationMethod,
|
|
1511
|
-
authentication: this.authentication,
|
|
1512
|
-
assertionMethod: this.assertionMethod,
|
|
1513
|
-
capabilityInvocation: this.capabilityInvocation,
|
|
1514
|
-
capabilityDelegation: this.capabilityDelegation,
|
|
1515
|
-
service: this.service,
|
|
1516
|
-
deactivated: this.deactivated
|
|
1517
|
-
};
|
|
1277
|
+
static fromJSON(object) {
|
|
1278
|
+
return new _GenesisDocument(object);
|
|
1518
1279
|
}
|
|
1519
1280
|
/**
|
|
1520
|
-
*
|
|
1521
|
-
* @param {
|
|
1522
|
-
* @
|
|
1523
|
-
* @returns {DidDocument} A new DidDocument with the placeholder ID.
|
|
1281
|
+
* Convert a GenesisDocument to genesis bytes.
|
|
1282
|
+
* @param {GenesisDocument} genesisDocument The GenesisDocument to convert.
|
|
1283
|
+
* @returns {Bytes} The genesis bytes.
|
|
1524
1284
|
*/
|
|
1525
|
-
static
|
|
1526
|
-
|
|
1527
|
-
const document = {
|
|
1528
|
-
id,
|
|
1529
|
-
verificationMethod: [
|
|
1530
|
-
new DidVerificationMethod({
|
|
1531
|
-
id,
|
|
1532
|
-
type: "Multikey",
|
|
1533
|
-
controller: id,
|
|
1534
|
-
publicKeyMultibase
|
|
1535
|
-
})
|
|
1536
|
-
],
|
|
1537
|
-
service
|
|
1538
|
-
};
|
|
1539
|
-
return new _DidDocument(document);
|
|
1285
|
+
static toGenesisBytes(genesisDocument) {
|
|
1286
|
+
return (0, import_common7.hash)((0, import_common7.canonicalize)(genesisDocument));
|
|
1540
1287
|
}
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
// src/core/identifier.ts
|
|
1291
|
+
var DID_PREFIX = "did:btcr2:";
|
|
1292
|
+
var Identifier = class _Identifier {
|
|
1541
1293
|
/**
|
|
1542
|
-
*
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1294
|
+
* Implements {@link https://dcdpr.github.io/did-btcr2/#didbtcr2-identifier-encoding | 3.2 did:btcr2 Identifier Encoding}.
|
|
1295
|
+
*
|
|
1296
|
+
* A did:btcr2 DID consists of a did:btcr2 prefix, followed by an id-bech32 value, which is a Bech32m encoding of:
|
|
1297
|
+
* - the specification version;
|
|
1298
|
+
* - the Bitcoin network identifier; and
|
|
1299
|
+
* - either:
|
|
1300
|
+
* - a key-value representing a secp256k1 public key; or
|
|
1301
|
+
* - a hash-value representing the hash of an initiating external DID document.
|
|
1302
|
+
*
|
|
1303
|
+
* @param {KeyBytes | DocumentBytes} genesisBytes The genesis bytes (public key or document bytes).
|
|
1304
|
+
* @param {DidCreateOptions} options The DID creation options.
|
|
1305
|
+
* @returns {string} The new did:btcr2 identifier.
|
|
1545
1306
|
*/
|
|
1546
|
-
static
|
|
1547
|
-
|
|
1307
|
+
static encode(genesisBytes, options) {
|
|
1308
|
+
const { idType, version = 1, network = "bitcoin" } = options;
|
|
1309
|
+
if (!(idType in import_common8.IdentifierTypes)) {
|
|
1310
|
+
throw new import_common8.IdentifierError('Expected "idType" to be "KEY" or "EXTERNAL"', import_common8.INVALID_DID, { idType });
|
|
1311
|
+
}
|
|
1312
|
+
if (version !== 1) {
|
|
1313
|
+
throw new import_common8.IdentifierError('Expected "version" to be 1', import_common8.INVALID_DID, { version });
|
|
1314
|
+
}
|
|
1315
|
+
if (typeof network !== "string") {
|
|
1316
|
+
throw new import_common8.IdentifierError('Expected "network" to be a known network name', import_common8.INVALID_DID, { network });
|
|
1317
|
+
}
|
|
1318
|
+
const networkValue = import_common8.BitcoinNetworkNames[network];
|
|
1319
|
+
if (networkValue === void 0) {
|
|
1320
|
+
throw new import_common8.IdentifierError('Invalid "network" name', import_common8.INVALID_DID, { network });
|
|
1321
|
+
}
|
|
1322
|
+
if (idType === "KEY") {
|
|
1323
|
+
try {
|
|
1324
|
+
new import_keypair2.CompressedSecp256k1PublicKey(genesisBytes);
|
|
1325
|
+
} catch {
|
|
1326
|
+
throw new import_common8.IdentifierError(
|
|
1327
|
+
'Expected "genesisBytes" to be a valid compressed secp256k1 public key',
|
|
1328
|
+
import_common8.INVALID_DID,
|
|
1329
|
+
{ genesisBytes }
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
} else if (genesisBytes.length !== 32) {
|
|
1333
|
+
throw new import_common8.IdentifierError(
|
|
1334
|
+
'Expected "genesisBytes" to be a 32-byte hash for EXTERNAL identifiers',
|
|
1335
|
+
import_common8.INVALID_DID,
|
|
1336
|
+
{ genesisBytes }
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
const hrp = idType === "KEY" ? "k" : "x";
|
|
1340
|
+
const firstByte = version - 1 << 4 | networkValue;
|
|
1341
|
+
const dataBytes = new Uint8Array([firstByte, ...genesisBytes]);
|
|
1342
|
+
return `${DID_PREFIX}${import_base.bech32m.encodeFromBytes(hrp, dataBytes)}`;
|
|
1548
1343
|
}
|
|
1549
1344
|
/**
|
|
1550
|
-
*
|
|
1551
|
-
* @
|
|
1345
|
+
* Implements {@link https://dcdpr.github.io/did-btcr2/#didbtcr2-identifier-decoding | 3.3 did:btcr2 Identifier Decoding}.
|
|
1346
|
+
* @param {string} identifier The BTCR2 DID to be parsed
|
|
1347
|
+
* @returns {DidComponents} The parsed identifier components. See {@link DidComponents} for details.
|
|
1348
|
+
* @throws {DidError} if an error occurs while parsing the identifier
|
|
1349
|
+
* @throws {DidErrorCode.InvalidDid} if identifier is invalid
|
|
1350
|
+
* @throws {DidErrorCode.MethodNotSupported} if the method is not supported
|
|
1552
1351
|
*/
|
|
1553
|
-
static
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1352
|
+
static decode(identifier) {
|
|
1353
|
+
const components = identifier.split(":");
|
|
1354
|
+
if (components.length !== 3) {
|
|
1355
|
+
throw new import_common8.IdentifierError(`Invalid did: ${identifier}`, import_common8.INVALID_DID, { identifier });
|
|
1356
|
+
}
|
|
1357
|
+
const [scheme, method, encoded] = components;
|
|
1358
|
+
if (scheme !== "did") {
|
|
1359
|
+
throw new import_common8.IdentifierError(`Invalid did: ${identifier}`, import_common8.INVALID_DID, { identifier });
|
|
1360
|
+
}
|
|
1361
|
+
if (method !== "btcr2") {
|
|
1362
|
+
throw new import_common8.IdentifierError(`Invalid did method: ${method}`, import_common8.METHOD_NOT_SUPPORTED, { identifier });
|
|
1363
|
+
}
|
|
1364
|
+
if (!encoded) {
|
|
1365
|
+
throw new import_common8.IdentifierError(`Invalid method-specific id: ${identifier}`, import_common8.INVALID_DID, { identifier });
|
|
1366
|
+
}
|
|
1367
|
+
if (encoded !== encoded.toLowerCase()) {
|
|
1368
|
+
throw new import_common8.IdentifierError(`Invalid method-specific id (must be lowercase): ${identifier}`, import_common8.INVALID_DID, { identifier });
|
|
1369
|
+
}
|
|
1370
|
+
const { prefix: hrp, bytes: dataBytes } = import_base.bech32m.decodeToBytes(encoded);
|
|
1371
|
+
if (!["x", "k"].includes(hrp)) {
|
|
1372
|
+
throw new import_common8.IdentifierError(`Invalid hrp: ${hrp}`, import_common8.INVALID_DID, { identifier });
|
|
1373
|
+
}
|
|
1374
|
+
if (!dataBytes || dataBytes.length < 1) {
|
|
1375
|
+
throw new import_common8.IdentifierError(`Failed to decode id: ${encoded}`, import_common8.INVALID_DID, { identifier });
|
|
1376
|
+
}
|
|
1377
|
+
const idType = hrp === "k" ? "KEY" : "EXTERNAL";
|
|
1378
|
+
const btcr2Version = dataBytes[0] >>> 4;
|
|
1379
|
+
if (btcr2Version !== 0) {
|
|
1380
|
+
throw new import_common8.IdentifierError(`Invalid btcr2_version (expected 0): ${btcr2Version}`, import_common8.INVALID_DID, { identifier });
|
|
1381
|
+
}
|
|
1382
|
+
const version = 1;
|
|
1383
|
+
const networkValue = dataBytes[0] & 15;
|
|
1384
|
+
const network = import_common8.BitcoinNetworkNames[networkValue];
|
|
1385
|
+
if (typeof network !== "string") {
|
|
1386
|
+
const reason = networkValue >= 12 ? "custom network not supported" : "reserved";
|
|
1387
|
+
throw new import_common8.IdentifierError(`Invalid network (${reason}): ${networkValue}`, import_common8.INVALID_DID, { identifier });
|
|
1388
|
+
}
|
|
1389
|
+
const genesisBytes = dataBytes.slice(1);
|
|
1390
|
+
if (idType === "KEY") {
|
|
1391
|
+
try {
|
|
1392
|
+
new import_keypair2.CompressedSecp256k1PublicKey(genesisBytes);
|
|
1393
|
+
} catch {
|
|
1394
|
+
throw new import_common8.IdentifierError(`Invalid genesisBytes: ${genesisBytes}`, import_common8.INVALID_DID, { identifier });
|
|
1557
1395
|
}
|
|
1396
|
+
} else if (genesisBytes.length !== 32) {
|
|
1397
|
+
throw new import_common8.IdentifierError(`Invalid genesisBytes: ${genesisBytes}`, import_common8.INVALID_DID, { identifier });
|
|
1558
1398
|
}
|
|
1559
|
-
return
|
|
1399
|
+
return { idType, hrp, version, network, genesisBytes };
|
|
1560
1400
|
}
|
|
1561
1401
|
/**
|
|
1562
|
-
* Validates a
|
|
1563
|
-
* @
|
|
1564
|
-
*
|
|
1565
|
-
*
|
|
1402
|
+
* Validates that a did:btcr2 identifier conforms to
|
|
1403
|
+
* {@link https://dcdpr.github.io/did-btcr2/#didbtcr2-identifier-decoding | 3.3 did:btcr2 Identifier Decoding}
|
|
1404
|
+
* and returns a report of the checks. The method does not throw on an invalid identifier.
|
|
1405
|
+
*
|
|
1406
|
+
* The checks run in this order: `prefix`, `lowercase`, `bech32m`, `version`, `network`,
|
|
1407
|
+
* `genesisBytes`, `roundTrip`, `genesisBytesMatch`, and `genesisDocument`. The run stops at the
|
|
1408
|
+
* first failed check. The `network` check accepts a named network only: a reserved value (6 to
|
|
1409
|
+
* 11) and a custom value (12 to 15) fail, because this implementation supports no custom network.
|
|
1410
|
+
* The `genesisBytesMatch` check runs only if `options.genesisBytes` is present: the supplied bytes
|
|
1411
|
+
* must equal the genesis bytes of the identifier, for a KEY or an EXTERNAL identifier. The
|
|
1412
|
+
* `genesisDocument` check runs only if `options.genesisDocument` is present. For an EXTERNAL
|
|
1413
|
+
* identifier it confirms that the document is a valid Genesis Document and that its canonical
|
|
1414
|
+
* SHA-256 hash equals the genesis bytes. For a KEY identifier it fails.
|
|
1415
|
+
*
|
|
1416
|
+
* @param {string} identifier The did:btcr2 identifier to validate.
|
|
1417
|
+
* @param {IdentifierValidateOptions} [options] The validation options.
|
|
1418
|
+
* @returns {IdentifierReport} The report. See {@link IdentifierReport} for details.
|
|
1566
1419
|
*/
|
|
1567
|
-
static
|
|
1568
|
-
|
|
1569
|
-
|
|
1420
|
+
static validate(identifier, options = {}) {
|
|
1421
|
+
const checks = [];
|
|
1422
|
+
const pass = (name, detail) => {
|
|
1423
|
+
checks.push(detail === void 0 ? { name, ok: true } : { name, ok: true, detail });
|
|
1424
|
+
};
|
|
1425
|
+
const fail = (name, detail, partial = {}) => {
|
|
1426
|
+
checks.push({ name, ok: false, detail });
|
|
1427
|
+
return { did: identifier, valid: false, ...partial, checks };
|
|
1428
|
+
};
|
|
1429
|
+
if (typeof identifier !== "string") {
|
|
1430
|
+
return fail("prefix", "The identifier is not a string.");
|
|
1570
1431
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1432
|
+
const parts = identifier.split(":");
|
|
1433
|
+
if (parts.length !== 3 || parts[0] !== "did" || parts[1] !== "btcr2") {
|
|
1434
|
+
return fail("prefix", `The identifier must be "${DID_PREFIX}" followed by the method-specific id.`);
|
|
1573
1435
|
}
|
|
1574
|
-
|
|
1575
|
-
|
|
1436
|
+
const encoded = parts[2];
|
|
1437
|
+
if (encoded.length === 0) {
|
|
1438
|
+
return fail("prefix", "The method-specific id is empty.");
|
|
1576
1439
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1440
|
+
pass("prefix");
|
|
1441
|
+
if (encoded !== encoded.toLowerCase()) {
|
|
1442
|
+
return fail("lowercase", "The method-specific id must be lowercase.");
|
|
1579
1443
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1444
|
+
pass("lowercase");
|
|
1445
|
+
let hrp;
|
|
1446
|
+
let dataBytes;
|
|
1447
|
+
try {
|
|
1448
|
+
({ prefix: hrp, bytes: dataBytes } = import_base.bech32m.decodeToBytes(encoded));
|
|
1449
|
+
} catch (error) {
|
|
1450
|
+
return fail("bech32m", `Bech32m decoding failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1582
1451
|
}
|
|
1583
|
-
|
|
1452
|
+
if (hrp !== "k" && hrp !== "x") {
|
|
1453
|
+
return fail("bech32m", `The hrp must be "k" or "x", got "${hrp}".`);
|
|
1454
|
+
}
|
|
1455
|
+
const idType = hrp === "k" ? import_common8.IdentifierTypes.KEY : import_common8.IdentifierTypes.EXTERNAL;
|
|
1456
|
+
if (dataBytes.length < 1) {
|
|
1457
|
+
return fail("bech32m", "The data bytes are empty.", { idType });
|
|
1458
|
+
}
|
|
1459
|
+
pass("bech32m", `hrp "${hrp}", ${dataBytes.length} data bytes`);
|
|
1460
|
+
const btcr2Version = dataBytes[0] >>> 4;
|
|
1461
|
+
if (btcr2Version !== 0) {
|
|
1462
|
+
return fail("version", `btcr2_version must be 0, got ${btcr2Version}.`, { idType });
|
|
1463
|
+
}
|
|
1464
|
+
pass("version", "btcr2_version 0 (version_number 1)");
|
|
1465
|
+
const networkValue = dataBytes[0] & 15;
|
|
1466
|
+
const network = import_common8.BitcoinNetworkNames[networkValue];
|
|
1467
|
+
if (typeof network !== "string") {
|
|
1468
|
+
const detail = networkValue >= 12 ? `network_value ${networkValue} is a custom network, not supported by this implementation.` : `network_value ${networkValue} is reserved.`;
|
|
1469
|
+
return fail("network", detail, { idType });
|
|
1470
|
+
}
|
|
1471
|
+
pass("network", `network_value ${networkValue} (${network})`);
|
|
1472
|
+
const genesisBytes = dataBytes.slice(1);
|
|
1473
|
+
if (idType === import_common8.IdentifierTypes.KEY) {
|
|
1474
|
+
try {
|
|
1475
|
+
new import_keypair2.CompressedSecp256k1PublicKey(genesisBytes);
|
|
1476
|
+
} catch {
|
|
1477
|
+
return fail(
|
|
1478
|
+
"genesisBytes",
|
|
1479
|
+
`Expected a 33-byte SEC compressed secp256k1 public key, got ${genesisBytes.length} bytes that are not a valid key.`,
|
|
1480
|
+
{ idType, network }
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
pass("genesisBytes", "33-byte SEC compressed secp256k1 public key");
|
|
1484
|
+
} else {
|
|
1485
|
+
if (genesisBytes.length !== 32) {
|
|
1486
|
+
return fail("genesisBytes", `Expected a 32-byte SHA-256 hash, got ${genesisBytes.length} bytes.`, { idType, network });
|
|
1487
|
+
}
|
|
1488
|
+
pass("genesisBytes", "32-byte SHA-256 hash");
|
|
1489
|
+
}
|
|
1490
|
+
let reEncoded;
|
|
1491
|
+
try {
|
|
1492
|
+
reEncoded = _Identifier.encode(genesisBytes, { idType, version: 1, network });
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
return fail("roundTrip", `Re-encoding failed: ${error instanceof Error ? error.message : String(error)}`, { idType, network });
|
|
1495
|
+
}
|
|
1496
|
+
if (reEncoded !== identifier) {
|
|
1497
|
+
return fail("roundTrip", `Re-encoding produced "${reEncoded}".`, { idType, network });
|
|
1498
|
+
}
|
|
1499
|
+
pass("roundTrip");
|
|
1500
|
+
if (options.genesisBytes !== void 0) {
|
|
1501
|
+
const supplied = options.genesisBytes;
|
|
1502
|
+
if (!(supplied instanceof Uint8Array)) {
|
|
1503
|
+
return fail("genesisBytesMatch", "The supplied genesis bytes are not a Uint8Array.", { idType, network });
|
|
1504
|
+
}
|
|
1505
|
+
if (supplied.length !== genesisBytes.length) {
|
|
1506
|
+
return fail(
|
|
1507
|
+
"genesisBytesMatch",
|
|
1508
|
+
`Expected ${genesisBytes.length} genesis bytes for a ${idType} identifier, got ${supplied.length}.`,
|
|
1509
|
+
{ idType, network }
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
if (!(0, import_utils4.equalBytes)(supplied, genesisBytes)) {
|
|
1513
|
+
return fail(
|
|
1514
|
+
"genesisBytesMatch",
|
|
1515
|
+
`The supplied genesis bytes ${import_base.hex.encode(supplied)} do not equal the genesis bytes of the identifier ${import_base.hex.encode(genesisBytes)}.`,
|
|
1516
|
+
{ idType, network }
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
pass("genesisBytesMatch", "The supplied genesis bytes equal the genesis bytes of the identifier.");
|
|
1520
|
+
}
|
|
1521
|
+
if (options.genesisDocument !== void 0) {
|
|
1522
|
+
const document = options.genesisDocument;
|
|
1523
|
+
if (idType === import_common8.IdentifierTypes.KEY) {
|
|
1524
|
+
return fail("genesisDocument", "A KEY identifier has no genesis document.", { idType, network });
|
|
1525
|
+
}
|
|
1526
|
+
const id = document.id;
|
|
1527
|
+
if (id !== ID_PLACEHOLDER_VALUE) {
|
|
1528
|
+
return fail("genesisDocument", `The genesis document id must be "${ID_PLACEHOLDER_VALUE}", got ${JSON.stringify(id)}.`, { idType, network });
|
|
1529
|
+
}
|
|
1530
|
+
try {
|
|
1531
|
+
GenesisDocument.fromJSON(document);
|
|
1532
|
+
} catch (error) {
|
|
1533
|
+
return fail("genesisDocument", `Invalid genesis document: ${error instanceof Error ? error.message : String(error)}`, { idType, network });
|
|
1534
|
+
}
|
|
1535
|
+
const documentHash = (0, import_common8.canonicalHashBytes)(document);
|
|
1536
|
+
if (!(0, import_utils4.equalBytes)(documentHash, genesisBytes)) {
|
|
1537
|
+
return fail(
|
|
1538
|
+
"genesisDocument",
|
|
1539
|
+
`The genesis document hash ${import_base.hex.encode(documentHash)} does not equal the genesis bytes ${import_base.hex.encode(genesisBytes)}.`,
|
|
1540
|
+
{ idType, network }
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
pass("genesisDocument", "The genesis document hashes to the genesis bytes.");
|
|
1544
|
+
}
|
|
1545
|
+
return { did: identifier, valid: true, idType, network, checks };
|
|
1584
1546
|
}
|
|
1585
1547
|
/**
|
|
1586
|
-
*
|
|
1587
|
-
* @
|
|
1588
|
-
* @param {DidDocument['@context']} context The context to validate.
|
|
1589
|
-
* @returns {boolean} True if the context is valid.
|
|
1548
|
+
* Generates a new did:btcr2 identifier based on a newly generated key pair.
|
|
1549
|
+
* @returns {string} The new did:btcr2 identifier.
|
|
1590
1550
|
*/
|
|
1591
|
-
static
|
|
1592
|
-
|
|
1593
|
-
|
|
1551
|
+
static generate() {
|
|
1552
|
+
const keyPair = import_keypair2.SchnorrKeyPair.generate();
|
|
1553
|
+
const did = this.encode(
|
|
1554
|
+
keyPair.publicKey.compressed,
|
|
1555
|
+
{
|
|
1556
|
+
idType: "KEY",
|
|
1557
|
+
version: 1,
|
|
1558
|
+
network: "regtest"
|
|
1559
|
+
}
|
|
1560
|
+
);
|
|
1561
|
+
return { keyPair: keyPair.exportJSON(), did };
|
|
1594
1562
|
}
|
|
1595
1563
|
/**
|
|
1596
|
-
*
|
|
1597
|
-
* @
|
|
1598
|
-
* @
|
|
1599
|
-
* @
|
|
1564
|
+
* Extracts the compressed secp256k1 public key from a KEY-type did:btcr2 identifier.
|
|
1565
|
+
* @param {string} did The did:btcr2 identifier to extract the public key from.
|
|
1566
|
+
* @returns {CompressedSecp256k1PublicKey} The compressed public key.
|
|
1567
|
+
* @throws {IdentifierError} If the DID is EXTERNAL type (genesis bytes are a hash, not a pubkey).
|
|
1600
1568
|
*/
|
|
1601
|
-
static
|
|
1602
|
-
|
|
1569
|
+
static getPublicKey(did) {
|
|
1570
|
+
const { idType, genesisBytes } = _Identifier.decode(did);
|
|
1571
|
+
if (idType !== "KEY") {
|
|
1572
|
+
throw new import_common8.IdentifierError(
|
|
1573
|
+
`Cannot extract public key from EXTERNAL DID: ${did}. EXTERNAL DIDs encode a document hash, not a public key.`,
|
|
1574
|
+
import_common8.INVALID_DID,
|
|
1575
|
+
{ did, idType }
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
return new import_keypair2.CompressedSecp256k1PublicKey(genesisBytes);
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Validates a did:btcr2 identifier.
|
|
1582
|
+
* @param {string} identifier The did:btcr2 identifier to validate.
|
|
1583
|
+
* @returns {boolean} True if the identifier is valid, false otherwise.
|
|
1584
|
+
*/
|
|
1585
|
+
static isValid(identifier) {
|
|
1603
1586
|
try {
|
|
1604
|
-
|
|
1587
|
+
this.decode(identifier);
|
|
1605
1588
|
return true;
|
|
1606
1589
|
} catch {
|
|
1607
1590
|
return false;
|
|
1608
1591
|
}
|
|
1609
1592
|
}
|
|
1593
|
+
};
|
|
1594
|
+
|
|
1595
|
+
// src/core/beacon/utils.ts
|
|
1596
|
+
var BeaconUtils = class {
|
|
1610
1597
|
/**
|
|
1611
|
-
*
|
|
1612
|
-
* @
|
|
1613
|
-
* @
|
|
1614
|
-
* @
|
|
1615
|
-
*/
|
|
1616
|
-
static isValidVerificationMethods(verificationMethod) {
|
|
1617
|
-
return Array.isArray(verificationMethod) && verificationMethod.every(isMultikeyVerificationMethod);
|
|
1618
|
-
}
|
|
1619
|
-
/**
|
|
1620
|
-
* Validates that the DID Document has valid services.
|
|
1621
|
-
* @private
|
|
1622
|
-
* @param {DidService[]} service The services to validate.
|
|
1623
|
-
* @returns {boolean} True if the services are valid.
|
|
1598
|
+
* Converts a BIP21 Bitcoin URI to a Bitcoin address
|
|
1599
|
+
* @param {string} uri The BIP21 Bitcoin URI to convert
|
|
1600
|
+
* @returns {string} The Bitcoin address extracted from the URI
|
|
1601
|
+
* @throws {DidMethodError} if the URI is not a valid Bitcoin URI
|
|
1624
1602
|
*/
|
|
1625
|
-
static
|
|
1626
|
-
|
|
1603
|
+
static parseBitcoinAddress(uri) {
|
|
1604
|
+
if (!uri.startsWith("bitcoin:")) {
|
|
1605
|
+
throw new import_common9.MethodError("Invalid Bitcoin URI format", "BEACON_SERVICE_ERROR", { uri });
|
|
1606
|
+
}
|
|
1607
|
+
return uri.replace("bitcoin:", "").split("?")[0];
|
|
1627
1608
|
}
|
|
1628
1609
|
/**
|
|
1629
|
-
* Validates
|
|
1630
|
-
* @
|
|
1631
|
-
* @
|
|
1632
|
-
* @returns {boolean} True if the verification relationships are valid.
|
|
1610
|
+
* Validates that the given object is a Beacon Service
|
|
1611
|
+
* @param {BeaconService} obj The object to validate
|
|
1612
|
+
* @returns {boolean} A boolean indicating whether the object is a Beacon Service
|
|
1633
1613
|
*/
|
|
1634
|
-
static
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
"capabilityDelegation"
|
|
1640
|
-
];
|
|
1641
|
-
const keys = Object.keys(didDocument);
|
|
1642
|
-
const availableKeys = possibleVerificationRelationships.filter((key) => keys.includes(key));
|
|
1643
|
-
return availableKeys.every((key) => {
|
|
1644
|
-
const value = didDocument[key];
|
|
1645
|
-
return value && Array.isArray(value) && value.every(
|
|
1646
|
-
(entry) => typeof entry === "string" || Appendix.isDidVerificationMethod(entry)
|
|
1647
|
-
);
|
|
1648
|
-
});
|
|
1614
|
+
static isBeaconService(obj) {
|
|
1615
|
+
if (!Appendix.isDidService(obj)) return false;
|
|
1616
|
+
if (!["SingletonBeacon", "CASBeacon", "SMTBeacon"].includes(obj.type)) return false;
|
|
1617
|
+
if ([obj.serviceEndpoint].flat().some((ep) => typeof ep === "string" && !ep.startsWith("bitcoin:"))) return false;
|
|
1618
|
+
return true;
|
|
1649
1619
|
}
|
|
1650
1620
|
/**
|
|
1651
|
-
*
|
|
1652
|
-
* @
|
|
1653
|
-
* @
|
|
1621
|
+
* Extracts the services from a given DID Document
|
|
1622
|
+
* @param {DidDocument} didDocument The DID Document to extract the services from
|
|
1623
|
+
* @returns {DidService[]} An array of DidService objects
|
|
1624
|
+
* @throws {TypeError} if the didDocument is not provided
|
|
1654
1625
|
*/
|
|
1655
|
-
static
|
|
1656
|
-
|
|
1657
|
-
didDocument.validateGenesis();
|
|
1658
|
-
} else {
|
|
1659
|
-
_DidDocument.isValid(didDocument);
|
|
1660
|
-
}
|
|
1661
|
-
return didDocument;
|
|
1626
|
+
static getBeaconServices(didDocument) {
|
|
1627
|
+
return didDocument.service.filter(this.isBeaconService) ?? [];
|
|
1662
1628
|
}
|
|
1663
1629
|
/**
|
|
1664
|
-
*
|
|
1665
|
-
* @
|
|
1630
|
+
* Create the 3 default Beacon Service Endpoints for a given `k` (public-key-based) identifier.
|
|
1631
|
+
* @param {string} did The DID for which to create the beacon services.
|
|
1632
|
+
* @returns {Array<Array<string>>} 2D Array of bitcoin addresses (p2pkh, p2wpkh, p2tr).
|
|
1633
|
+
* @throws {DidMethodError} if the bitcoin address is invalid.
|
|
1666
1634
|
*/
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1635
|
+
static createBeaconServices(did, beaconType) {
|
|
1636
|
+
try {
|
|
1637
|
+
const addrTypes = ["p2pkh", "p2wpkh", "p2tr"];
|
|
1638
|
+
return addrTypes.map(
|
|
1639
|
+
(addrType) => this.createBeaconService(did, addrType, beaconType)
|
|
1640
|
+
);
|
|
1641
|
+
} catch (error) {
|
|
1642
|
+
throw new BeaconError(
|
|
1643
|
+
"Failed to create beacon services: " + error.message,
|
|
1644
|
+
"BEACON_SERVICE_ERROR",
|
|
1645
|
+
{ did, beaconType }
|
|
1646
|
+
);
|
|
1679
1647
|
}
|
|
1680
|
-
return true;
|
|
1681
1648
|
}
|
|
1682
1649
|
/**
|
|
1683
|
-
*
|
|
1684
|
-
* @
|
|
1650
|
+
* Generate a set of Beacon Services for a given public key.
|
|
1651
|
+
* @param {string} did The did for the beacon service.
|
|
1652
|
+
* @param {string} addressType The type of bitcoin address to generate (p2pkh, p2wpkh, p2tr).
|
|
1653
|
+
* @param {string} beaconType The type of beacon service to create.
|
|
1654
|
+
* @returns {BeaconService} A BeaconService object.
|
|
1655
|
+
* @throws {DidMethodError} if the bitcoin address is invalid.
|
|
1685
1656
|
*/
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1657
|
+
static createBeaconService(did, addressType, beaconType) {
|
|
1658
|
+
try {
|
|
1659
|
+
const components = Identifier.decode(did);
|
|
1660
|
+
const network = (0, import_bitcoin3.getNetwork)(components.network);
|
|
1661
|
+
const pubkey = components.genesisBytes;
|
|
1662
|
+
const id = `${did}#initial${addressType.toUpperCase()}`;
|
|
1663
|
+
const address = addressType === "p2tr" ? (0, import_btc_signer3.p2tr)(pubkey.slice(1, 33), void 0, network).address : addressType === "p2wpkh" ? (0, import_btc_signer3.p2wpkh)(pubkey, network).address : (0, import_btc_signer3.p2pkh)(pubkey, network).address;
|
|
1664
|
+
const serviceEndpoint = `bitcoin:${address}`;
|
|
1665
|
+
return { id, type: beaconType, serviceEndpoint };
|
|
1666
|
+
} catch (error) {
|
|
1667
|
+
throw new BeaconError(
|
|
1668
|
+
"Failed to create beacon service: " + error.message,
|
|
1669
|
+
"BEACON_SERVICE_ERROR",
|
|
1670
|
+
{ did, beaconType }
|
|
1671
|
+
);
|
|
1689
1672
|
}
|
|
1690
|
-
return new GenesisDocument(this);
|
|
1691
|
-
}
|
|
1692
|
-
};
|
|
1693
|
-
var GenesisDocument = class _GenesisDocument extends DidDocument {
|
|
1694
|
-
constructor(document) {
|
|
1695
|
-
super(document);
|
|
1696
1673
|
}
|
|
1697
1674
|
/**
|
|
1698
|
-
*
|
|
1699
|
-
* @param did The DID to
|
|
1700
|
-
* @returns {
|
|
1675
|
+
* Generate three default Beacon Service Endpoints for a given `k` (public-key-based) identifier.
|
|
1676
|
+
* @param {string} did The DID for which to create the beacon services.
|
|
1677
|
+
* @returns {Array<Array<string>>} 2D Array of bitcoin addresses (p2pkh, p2wpkh, p2tr).
|
|
1678
|
+
* @throws {DidMethodError} if the bitcoin address is invalid.
|
|
1701
1679
|
*/
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1680
|
+
static generateBeaconServices({ id, publicKey, network, beaconType }) {
|
|
1681
|
+
try {
|
|
1682
|
+
const p2pkhAddr = (0, import_btc_signer3.p2pkh)(publicKey, network).address;
|
|
1683
|
+
const p2wpkhAddr = (0, import_btc_signer3.p2wpkh)(publicKey, network).address;
|
|
1684
|
+
const p2trAddr = (0, import_btc_signer3.p2tr)(publicKey.slice(1, 33), void 0, network).address;
|
|
1685
|
+
if (!p2pkhAddr || !p2wpkhAddr || !p2trAddr) {
|
|
1686
|
+
throw new import_common9.DidMethodError("Failed to generate bitcoin addresses");
|
|
1687
|
+
}
|
|
1688
|
+
return [
|
|
1689
|
+
{
|
|
1690
|
+
id: `${id}#initialP2PKH`,
|
|
1691
|
+
type: beaconType,
|
|
1692
|
+
serviceEndpoint: `bitcoin:${p2pkhAddr}`
|
|
1693
|
+
},
|
|
1694
|
+
{
|
|
1695
|
+
id: `${id}#initialP2WPKH`,
|
|
1696
|
+
type: beaconType,
|
|
1697
|
+
serviceEndpoint: `bitcoin:${p2wpkhAddr}`
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
id: `${id}#initialP2TR`,
|
|
1701
|
+
type: beaconType,
|
|
1702
|
+
serviceEndpoint: `bitcoin:${p2trAddr}`
|
|
1703
|
+
}
|
|
1704
|
+
];
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
throw new BeaconError(
|
|
1707
|
+
"Failed to create beacon services: " + error.message,
|
|
1708
|
+
"BEACON_SERVICE_ERROR",
|
|
1709
|
+
{ id, publicKey, network, beaconType }
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1706
1712
|
}
|
|
1707
1713
|
/**
|
|
1708
|
-
*
|
|
1709
|
-
* @param {
|
|
1710
|
-
* @returns {
|
|
1714
|
+
* Convert beacon service endpoints from BIP-21 URIs to addresses.
|
|
1715
|
+
* @param {BeaconService} beacon The beacon service to parse.
|
|
1716
|
+
* @returns {BeaconServiceAddress} The beacon service with the address field extracted from the serviceEndpoint.
|
|
1711
1717
|
*/
|
|
1712
|
-
static
|
|
1713
|
-
|
|
1714
|
-
return new _GenesisDocument(intermediateDocument);
|
|
1718
|
+
static parseBeaconServiceEndpoint(beacon) {
|
|
1719
|
+
return { ...beacon, serviceEndpoint: beacon.serviceEndpoint.replace("bitcoin:", "") };
|
|
1715
1720
|
}
|
|
1716
1721
|
/**
|
|
1717
|
-
*
|
|
1718
|
-
* @param {
|
|
1719
|
-
* @
|
|
1720
|
-
* @param {Array<BeaconService>} service The service to be included in the document.
|
|
1721
|
-
* @returns {GenesisDocument} A new GenesisDocument with the placeholder ID.
|
|
1722
|
+
* Get the beacon service ids from a list of beacon services.
|
|
1723
|
+
* @param {DidDocument} didDocument The DID Document to extract the services from.
|
|
1724
|
+
* @returns {string[]} An array of beacon service ids.
|
|
1722
1725
|
*/
|
|
1723
|
-
static
|
|
1724
|
-
return
|
|
1726
|
+
static getBeaconServiceIds(didDocument) {
|
|
1727
|
+
return this.getBeaconServices(didDocument).map((beacon) => beacon.id);
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
|
|
1731
|
+
// src/core/beacon/signal-discovery.ts
|
|
1732
|
+
var BEACON_SIGNAL_SCRIPT = /^6a20([0-9a-f]{64})$/i;
|
|
1733
|
+
function extractOpReturnSignalHash(scriptPubKey) {
|
|
1734
|
+
if (!scriptPubKey) {
|
|
1735
|
+
return null;
|
|
1736
|
+
}
|
|
1737
|
+
const signal = BEACON_SIGNAL_SCRIPT.exec(scriptPubKey.trim());
|
|
1738
|
+
if (!signal) {
|
|
1739
|
+
return null;
|
|
1725
1740
|
}
|
|
1741
|
+
return signal[1].toLowerCase();
|
|
1742
|
+
}
|
|
1743
|
+
var BeaconSignalDiscovery = class _BeaconSignalDiscovery {
|
|
1726
1744
|
/**
|
|
1727
|
-
*
|
|
1728
|
-
*
|
|
1729
|
-
*
|
|
1745
|
+
* Determines whether a candidate transaction spends an output controlled by the given
|
|
1746
|
+
* beacon address.
|
|
1747
|
+
*
|
|
1748
|
+
* A Beacon Signal is a transaction that *spends from* a Beacon Address, but an address
|
|
1749
|
+
* transaction listing returns every transaction touching the address in either
|
|
1750
|
+
* direction. Without this check, anyone able to pay dust to a beacon address could
|
|
1751
|
+
* attach an arbitrary 32-byte OP_RETURN and have it read as a signal, so the input side
|
|
1752
|
+
* has to be inspected before a transaction is treated as one.
|
|
1753
|
+
*
|
|
1754
|
+
* Esplora embeds the spent output in `vin[].prevout`; when a backend omits it the
|
|
1755
|
+
* funding transaction is fetched instead, so a missing field cannot silently drop a
|
|
1756
|
+
* real signal.
|
|
1757
|
+
*
|
|
1758
|
+
* @param {RawTransactionRest} tx The candidate transaction.
|
|
1759
|
+
* @param {string} address The beacon address the transaction must spend from.
|
|
1760
|
+
* @param {BitcoinConnection} bitcoin Bitcoin network connection to use for REST calls.
|
|
1761
|
+
* @returns {Promise<boolean>} True if at least one input spends an output of the beacon address.
|
|
1730
1762
|
*/
|
|
1731
|
-
static
|
|
1732
|
-
const
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
const services = [{
|
|
1736
|
-
id: `${id}#service-0`,
|
|
1737
|
-
serviceEndpoint: `bitcoin:${address}`,
|
|
1738
|
-
type: "SingletonBeacon"
|
|
1739
|
-
}];
|
|
1740
|
-
const relationships = {
|
|
1741
|
-
authentication: [`${id}#key-0`],
|
|
1742
|
-
assertionMethod: [`${id}#key-0`],
|
|
1743
|
-
capabilityInvocation: [`${id}#key-0`],
|
|
1744
|
-
capabilityDelegation: [`${id}#key-0`]
|
|
1745
|
-
};
|
|
1746
|
-
const verificationMethod = [
|
|
1747
|
-
{
|
|
1748
|
-
id: `${id}#key-0`,
|
|
1749
|
-
type: "Multikey",
|
|
1750
|
-
controller: id,
|
|
1751
|
-
publicKeyMultibase: pk.multibase.encoded
|
|
1763
|
+
static async spendsFromAddress(tx, address, bitcoin) {
|
|
1764
|
+
for (const vin of tx.vin ?? []) {
|
|
1765
|
+
if (vin.is_coinbase) {
|
|
1766
|
+
continue;
|
|
1752
1767
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1768
|
+
let prevout = vin.prevout;
|
|
1769
|
+
if (!prevout && vin.txid) {
|
|
1770
|
+
const fundingTx = await bitcoin.rest.transaction.get(vin.txid);
|
|
1771
|
+
prevout = fundingTx?.vout?.[vin.vout];
|
|
1772
|
+
}
|
|
1773
|
+
if (prevout?.scriptpubkey_address === address) {
|
|
1774
|
+
return true;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return false;
|
|
1755
1778
|
}
|
|
1756
1779
|
/**
|
|
1757
|
-
*
|
|
1758
|
-
*
|
|
1759
|
-
*
|
|
1780
|
+
* Retrieves the beacon signals for the given array of BeaconService objects
|
|
1781
|
+
* using an esplora/electrs REST API connection via a bitcoin I/O driver.
|
|
1782
|
+
*
|
|
1783
|
+
* The address listing includes mempool transactions. The method skips a
|
|
1784
|
+
* transaction whose `status.confirmed` is not `true`. A mempool transaction
|
|
1785
|
+
* has no block height and no block time, so it cannot carry block metadata.
|
|
1786
|
+
* The specification also says that a resolver must not process an
|
|
1787
|
+
* unconfirmed transaction. An absent flag counts as unconfirmed, as it does
|
|
1788
|
+
* for UTXO selection. The check runs before the OP_RETURN parse, so a
|
|
1789
|
+
* mempool transaction costs no prevout fetch. The {@link fullnode} path
|
|
1790
|
+
* needs no such check: it walks mined blocks only.
|
|
1791
|
+
*
|
|
1792
|
+
* The `confirmations` count uses the block count fetched before the listing.
|
|
1793
|
+
* A block that arrives between the two calls yields a count of `0` for its
|
|
1794
|
+
* transactions. The resolver then excludes them, because its minimum is at
|
|
1795
|
+
* least `1`. An under-count is the safe direction, so keep that order.
|
|
1796
|
+
* @param {Array<BeaconService>} beaconServices Array of BeaconService objects to retrieve signals for
|
|
1797
|
+
* @param {BitcoinConnection} bitcoin Bitcoin network connection to use for REST calls
|
|
1798
|
+
* @returns {Promise<Map<BeaconService, Array<BeaconSignal>>>} Map of beacon service to its discovered signals
|
|
1760
1799
|
*/
|
|
1761
|
-
static
|
|
1762
|
-
|
|
1800
|
+
static async indexer(beaconServices, bitcoin) {
|
|
1801
|
+
const beaconServiceSignals = /* @__PURE__ */ new Map();
|
|
1802
|
+
const currentBlockCount = await bitcoin.rest.block.count();
|
|
1803
|
+
for (const beaconService of beaconServices) {
|
|
1804
|
+
beaconServiceSignals.set(beaconService, []);
|
|
1805
|
+
const beaconAddress = BeaconUtils.parseBitcoinAddress(beaconService.serviceEndpoint);
|
|
1806
|
+
const beaconSignals = await bitcoin.rest.address.getTxs(beaconAddress);
|
|
1807
|
+
if (!beaconSignals || !beaconSignals.length) {
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
for (const beaconSignal of beaconSignals) {
|
|
1811
|
+
const status = beaconSignal.status;
|
|
1812
|
+
if (status.confirmed !== true) {
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
const lastSignalVout = beaconSignal.vout.slice(-1)[0];
|
|
1816
|
+
if (!lastSignalVout) {
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
const updateHash = extractOpReturnSignalHash(lastSignalVout.scriptpubkey);
|
|
1820
|
+
if (!updateHash) {
|
|
1821
|
+
continue;
|
|
1822
|
+
}
|
|
1823
|
+
if (!await _BeaconSignalDiscovery.spendsFromAddress(beaconSignal, beaconAddress, bitcoin)) {
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
const confirmations = currentBlockCount - status.block_height + 1;
|
|
1827
|
+
beaconServiceSignals.get(beaconService)?.push({
|
|
1828
|
+
tx: beaconSignal,
|
|
1829
|
+
signalBytes: updateHash,
|
|
1830
|
+
blockMetadata: {
|
|
1831
|
+
confirmations,
|
|
1832
|
+
height: status.block_height,
|
|
1833
|
+
time: status.block_time
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return beaconServiceSignals;
|
|
1763
1839
|
}
|
|
1764
1840
|
/**
|
|
1765
|
-
*
|
|
1766
|
-
* @param {
|
|
1767
|
-
* @
|
|
1841
|
+
* Traverse the full blockchain from genesis to chain top looking for beacon signals.
|
|
1842
|
+
* @param {Array<BeaconService>} beaconServices Array of BeaconService objects to search for signals.
|
|
1843
|
+
* @param {BitcoinConnection} bitcoin Bitcoin network connection to use for RPC calls.
|
|
1844
|
+
* @returns {Promise<Map<BeaconService, Array<BeaconSignal>>>} Map of beacon service to its discovered signals.
|
|
1768
1845
|
*/
|
|
1769
|
-
static
|
|
1770
|
-
|
|
1846
|
+
static async fullnode(beaconServices, bitcoin) {
|
|
1847
|
+
const beaconServiceSignals = /* @__PURE__ */ new Map();
|
|
1848
|
+
for (const beaconService of beaconServices) {
|
|
1849
|
+
beaconServiceSignals.set(beaconService, []);
|
|
1850
|
+
}
|
|
1851
|
+
const rpc = bitcoin.rpc;
|
|
1852
|
+
if (!rpc) {
|
|
1853
|
+
throw new import_common10.ResolveError("RPC connection is not available", "RPC_CONNECTION_ERROR", bitcoin);
|
|
1854
|
+
}
|
|
1855
|
+
const targetHeight = await rpc.getBlockCount();
|
|
1856
|
+
const beaconServicesMap = new Map(
|
|
1857
|
+
beaconServices.map((service) => [BeaconUtils.parseBitcoinAddress(service.serviceEndpoint), service])
|
|
1858
|
+
);
|
|
1859
|
+
let height = 0;
|
|
1860
|
+
let block = await bitcoin.rpc.getBlock({ height });
|
|
1861
|
+
console.info(`Searching for beacon signals, please wait ...`);
|
|
1862
|
+
while (block.height <= targetHeight) {
|
|
1863
|
+
for (const tx of block.tx) {
|
|
1864
|
+
if (tx.txid === import_bitcoin4.GENESIS_TX_ID) {
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
const lastSignalVout = tx.vout.slice(-1)[0];
|
|
1868
|
+
if (!lastSignalVout) {
|
|
1869
|
+
continue;
|
|
1870
|
+
}
|
|
1871
|
+
const updateHash = extractOpReturnSignalHash(lastSignalVout.scriptPubKey?.hex);
|
|
1872
|
+
if (!updateHash) {
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
const signaled = /* @__PURE__ */ new Set();
|
|
1876
|
+
for (const vin of tx.vin) {
|
|
1877
|
+
if (vin.coinbase) {
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
if (vin.txinwitness && vin.txinwitness.length === 1 && vin.txinwitness[0] === import_bitcoin4.TXIN_WITNESS_COINBASE) {
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
if (!vin.txid) {
|
|
1884
|
+
continue;
|
|
1885
|
+
}
|
|
1886
|
+
if (vin.vout === void 0) {
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
const prevout = await rpc.getRawTransaction(vin.txid, 2);
|
|
1890
|
+
if (!prevout.vout[vin.vout]) {
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
const scriptPubKey = prevout.vout[vin.vout].scriptPubKey;
|
|
1894
|
+
if (!scriptPubKey.address) {
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
const beaconService = beaconServicesMap.get(scriptPubKey.address);
|
|
1898
|
+
if (!beaconService || signaled.has(beaconService)) {
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
1901
|
+
signaled.add(beaconService);
|
|
1902
|
+
console.info(`Tx ${tx.txid} contains beacon address ${scriptPubKey.address}`);
|
|
1903
|
+
beaconServiceSignals.get(beaconService)?.push({
|
|
1904
|
+
tx,
|
|
1905
|
+
signalBytes: updateHash,
|
|
1906
|
+
blockMetadata: {
|
|
1907
|
+
height: block.height,
|
|
1908
|
+
time: block.time,
|
|
1909
|
+
confirmations: block.confirmations
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
;
|
|
1914
|
+
}
|
|
1915
|
+
height += 1;
|
|
1916
|
+
if (height > targetHeight) {
|
|
1917
|
+
console.info(`Chain tip reached ${height}, breaking ...`);
|
|
1918
|
+
break;
|
|
1919
|
+
}
|
|
1920
|
+
block = await rpc.getBlock({ height });
|
|
1921
|
+
}
|
|
1922
|
+
return beaconServiceSignals;
|
|
1771
1923
|
}
|
|
1772
1924
|
};
|
|
1773
1925
|
|
|
1926
|
+
// src/core/did-sender-resolver.ts
|
|
1927
|
+
var import_common14 = require("@did-btcr2/common");
|
|
1928
|
+
var import_cryptosuite3 = require("@did-btcr2/cryptosuite");
|
|
1929
|
+
var import_keypair4 = require("@did-btcr2/keypair");
|
|
1930
|
+
|
|
1931
|
+
// src/core/resolver.ts
|
|
1932
|
+
var import_bitcoin5 = require("@did-btcr2/bitcoin");
|
|
1933
|
+
var import_common13 = require("@did-btcr2/common");
|
|
1934
|
+
var import_cryptosuite2 = require("@did-btcr2/cryptosuite");
|
|
1935
|
+
var import_keypair3 = require("@did-btcr2/keypair");
|
|
1936
|
+
|
|
1937
|
+
// src/did-btcr2.ts
|
|
1938
|
+
var import_common12 = require("@did-btcr2/common");
|
|
1939
|
+
var import_dids2 = require("@web5/dids");
|
|
1940
|
+
|
|
1774
1941
|
// src/core/updater.ts
|
|
1942
|
+
var import_common11 = require("@did-btcr2/common");
|
|
1943
|
+
var import_cryptosuite = require("@did-btcr2/cryptosuite");
|
|
1775
1944
|
var Updater = class _Updater {
|
|
1776
1945
|
#state = { phase: "Construct" };
|
|
1777
1946
|
#sourceDocument;
|
|
@@ -2116,7 +2285,8 @@ var DidBtcr2 = class {
|
|
|
2116
2285
|
versionId: resolutionOptions.versionId,
|
|
2117
2286
|
versionTime: resolutionOptions.versionTime,
|
|
2118
2287
|
genesisDocument: resolutionOptions.sidecar?.genesisDocument,
|
|
2119
|
-
maxDiscoveryRounds: resolutionOptions.maxDiscoveryRounds
|
|
2288
|
+
maxDiscoveryRounds: resolutionOptions.maxDiscoveryRounds,
|
|
2289
|
+
minConf: resolutionOptions.minConf
|
|
2120
2290
|
});
|
|
2121
2291
|
}
|
|
2122
2292
|
/**
|
|
@@ -2230,7 +2400,8 @@ var DidBtcr2 = class {
|
|
|
2230
2400
|
};
|
|
2231
2401
|
|
|
2232
2402
|
// src/core/resolver.ts
|
|
2233
|
-
var
|
|
2403
|
+
var import_utils7 = require("@noble/curves/utils.js");
|
|
2404
|
+
var DEFAULT_MIN_CONF = 6;
|
|
2234
2405
|
function isRecord(value) {
|
|
2235
2406
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2236
2407
|
}
|
|
@@ -2245,6 +2416,16 @@ function isSMTProof(value) {
|
|
|
2245
2416
|
if (!isRecord(value)) return false;
|
|
2246
2417
|
return typeof value.id === "string" && typeof value.collapsed === "string" && Array.isArray(value.hashes);
|
|
2247
2418
|
}
|
|
2419
|
+
function validateMinConf(value) {
|
|
2420
|
+
if (value === void 0) return DEFAULT_MIN_CONF;
|
|
2421
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1) return value;
|
|
2422
|
+
const shown = typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
2423
|
+
throw new import_common13.ResolveError(
|
|
2424
|
+
`Invalid resolution option minConf: expected a positive integer (minimum 1), got ${shown}.`,
|
|
2425
|
+
import_common13.INVALID_OPTIONS,
|
|
2426
|
+
{ minConf: value }
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2248
2429
|
var Resolver = class _Resolver {
|
|
2249
2430
|
// --- Immutable inputs ---
|
|
2250
2431
|
#didComponents;
|
|
@@ -2284,6 +2465,14 @@ var Resolver = class _Resolver {
|
|
|
2284
2465
|
#maxDiscoveryRounds;
|
|
2285
2466
|
/** Count of beacon-discovery passes driven by updates adding new beacon services. */
|
|
2286
2467
|
#discoveryRounds = 0;
|
|
2468
|
+
/**
|
|
2469
|
+
* Minimum block confirmations a Beacon Signal must have before this resolver
|
|
2470
|
+
* processes it: `ResolutionOptions.minConf`, default {@link DEFAULT_MIN_CONF}.
|
|
2471
|
+
* Applied at signal intake in the BeaconProcess phase. A signal below the
|
|
2472
|
+
* threshold is excluded from the resolution; the rest of the signals are
|
|
2473
|
+
* processed.
|
|
2474
|
+
*/
|
|
2475
|
+
#minConf;
|
|
2287
2476
|
/**
|
|
2288
2477
|
* @internal Use {@link DidBtcr2.resolve} to create instances.
|
|
2289
2478
|
*/
|
|
@@ -2295,6 +2484,7 @@ var Resolver = class _Resolver {
|
|
|
2295
2484
|
this.#versionTime = options?.versionTime;
|
|
2296
2485
|
const rounds = options?.maxDiscoveryRounds;
|
|
2297
2486
|
this.#maxDiscoveryRounds = typeof rounds === "number" && rounds > 0 ? rounds : Infinity;
|
|
2487
|
+
this.#minConf = validateMinConf(options?.minConf);
|
|
2298
2488
|
if (options?.genesisDocument) {
|
|
2299
2489
|
this.#providedGenesisDocument = options.genesisDocument;
|
|
2300
2490
|
}
|
|
@@ -2335,7 +2525,7 @@ var Resolver = class _Resolver {
|
|
|
2335
2525
|
*/
|
|
2336
2526
|
static external(didComponents, genesisDocument) {
|
|
2337
2527
|
const genesisDocumentHash = (0, import_common13.canonicalHashBytes)(genesisDocument);
|
|
2338
|
-
if (!(0,
|
|
2528
|
+
if (!(0, import_utils7.equalBytes)(didComponents.genesisBytes, genesisDocumentHash)) {
|
|
2339
2529
|
throw new import_common13.ResolveError(
|
|
2340
2530
|
`Initial document mismatch: genesisBytes !== genesisDocumentHash`,
|
|
2341
2531
|
import_common13.INVALID_DID_DOCUMENT,
|
|
@@ -2384,6 +2574,10 @@ var Resolver = class _Resolver {
|
|
|
2384
2574
|
* Version counter and update-hash history carried from earlier discovery rounds.
|
|
2385
2575
|
* Standalone callers omit it and start fresh at version 1 with an empty history.
|
|
2386
2576
|
* @returns {DidResolutionResponse} The updated DID Document, number of confirmations, and version id.
|
|
2577
|
+
*
|
|
2578
|
+
* Confirmation depth is not checked here. The BeaconProcess phase excludes a
|
|
2579
|
+
* signal below `ResolutionOptions.minConf` before its update reaches this method,
|
|
2580
|
+
* so every tuple here comes from a block at or above the threshold.
|
|
2387
2581
|
*/
|
|
2388
2582
|
static updates(currentDocument, unsortedUpdates, versionTime, versionId, resolutionState = { currentVersionId: 1, updateHashHistory: [] }) {
|
|
2389
2583
|
let currentVersionId = resolutionState.currentVersionId;
|
|
@@ -2416,7 +2610,7 @@ var Resolver = class _Resolver {
|
|
|
2416
2610
|
}
|
|
2417
2611
|
if (update.targetVersionId === currentVersionId + 1) {
|
|
2418
2612
|
const sourceHashBytes = (0, import_common13.decode)(update.sourceHash, "base64urlnopad");
|
|
2419
|
-
if (!(0,
|
|
2613
|
+
if (!(0, import_utils7.equalBytes)(sourceHashBytes, currentDocumentHash)) {
|
|
2420
2614
|
throw new import_common13.ResolveError(
|
|
2421
2615
|
`Hash mismatch: update.sourceHash !== currentDocumentHash`,
|
|
2422
2616
|
import_common13.INVALID_DID_UPDATE,
|
|
@@ -2480,7 +2674,7 @@ var Resolver = class _Resolver {
|
|
|
2480
2674
|
}
|
|
2481
2675
|
);
|
|
2482
2676
|
}
|
|
2483
|
-
if (!(0,
|
|
2677
|
+
if (!(0, import_utils7.equalBytes)(historicalUpdateHash, unsignedUpdateHash)) {
|
|
2484
2678
|
throw new import_common13.ResolveError(
|
|
2485
2679
|
`Invalid duplicate: unsigned update hash does not match historical hash`,
|
|
2486
2680
|
import_common13.LATE_PUBLISHING_ERROR,
|
|
@@ -2547,7 +2741,7 @@ var Resolver = class _Resolver {
|
|
|
2547
2741
|
DidDocument.validate(updatedDocument);
|
|
2548
2742
|
const currentDocumentHash = (0, import_common13.canonicalHashBytes)(updatedDocument);
|
|
2549
2743
|
const updateTargetHash = (0, import_common13.decode)(update.targetHash);
|
|
2550
|
-
if (!(0,
|
|
2744
|
+
if (!(0, import_utils7.equalBytes)(updateTargetHash, currentDocumentHash)) {
|
|
2551
2745
|
throw new import_common13.ResolveError(
|
|
2552
2746
|
`Invalid update: update.targetHash !== currentDocumentHash`,
|
|
2553
2747
|
import_common13.INVALID_DID_UPDATE,
|
|
@@ -2611,8 +2805,10 @@ var Resolver = class _Resolver {
|
|
|
2611
2805
|
const allNeeds = [];
|
|
2612
2806
|
for (const [service, signals] of this.#beaconServicesSignals) {
|
|
2613
2807
|
if (this.#processedServices.has(service.id) || !signals.length) continue;
|
|
2808
|
+
const eligible = this.#eligibleSignals(signals);
|
|
2809
|
+
if (!eligible.length) continue;
|
|
2614
2810
|
const beacon = BeaconFactory.establish(service, this.#currentDocument.id);
|
|
2615
|
-
const result = beacon.processSignals(
|
|
2811
|
+
const result = beacon.processSignals(eligible, this.#sidecarData);
|
|
2616
2812
|
if (result.needs.length > 0) {
|
|
2617
2813
|
allNeeds.push(...result.needs);
|
|
2618
2814
|
} else {
|
|
@@ -2676,6 +2872,41 @@ var Resolver = class _Resolver {
|
|
|
2676
2872
|
}
|
|
2677
2873
|
}
|
|
2678
2874
|
}
|
|
2875
|
+
/**
|
|
2876
|
+
* Return the signals of one beacon service that resolution may process: the
|
|
2877
|
+
* signals with at least `#minConf` confirmations. The specification removes a
|
|
2878
|
+
* transaction below the threshold from the set of Beacon Signals, so an
|
|
2879
|
+
* excluded signal emits no data need and applies no update. A signal with no
|
|
2880
|
+
* integer confirmation count is excluded too: that is a mempool transaction
|
|
2881
|
+
* from a driver that did not skip it.
|
|
2882
|
+
*
|
|
2883
|
+
* An eligible signal must carry a finite block height and block time. A
|
|
2884
|
+
* signal that passes the count but lacks them is malformed. It fails fast
|
|
2885
|
+
* here with a typed error, in the style of the {@link provide} guards, and
|
|
2886
|
+
* not later with an invalid date inside {@link updates}.
|
|
2887
|
+
* @param {Array<BeaconSignal>} signals The signals the caller provided for one service.
|
|
2888
|
+
* @returns {Array<BeaconSignal>} The signals at or above the threshold, in the given order.
|
|
2889
|
+
* @throws {ResolveError} `INVALID_DID_UPDATE` for an eligible signal with no valid block metadata.
|
|
2890
|
+
*/
|
|
2891
|
+
#eligibleSignals(signals) {
|
|
2892
|
+
const eligible = [];
|
|
2893
|
+
for (const signal of signals) {
|
|
2894
|
+
const block = signal.blockMetadata;
|
|
2895
|
+
const confirmations = block?.confirmations;
|
|
2896
|
+
if (!Number.isInteger(confirmations) || confirmations < this.#minConf) {
|
|
2897
|
+
continue;
|
|
2898
|
+
}
|
|
2899
|
+
if (!Number.isFinite(block?.height) || !Number.isFinite(block?.time)) {
|
|
2900
|
+
throw new import_common13.ResolveError(
|
|
2901
|
+
`Beacon signal ${signal.signalBytes} has ${confirmations} confirmations but no valid block height or block time.`,
|
|
2902
|
+
import_common13.INVALID_DID_UPDATE,
|
|
2903
|
+
{ signalBytes: signal.signalBytes, confirmations, height: block?.height, time: block?.time }
|
|
2904
|
+
);
|
|
2905
|
+
}
|
|
2906
|
+
eligible.push(signal);
|
|
2907
|
+
}
|
|
2908
|
+
return eligible;
|
|
2909
|
+
}
|
|
2679
2910
|
provide(need, data) {
|
|
2680
2911
|
switch (need.kind) {
|
|
2681
2912
|
case "NeedGenesisDocument": {
|
|
@@ -2869,6 +3100,7 @@ var DidDocumentBuilder = class {
|
|
|
2869
3100
|
CASBeaconError,
|
|
2870
3101
|
CHANGE_OUTPUT_VBYTES,
|
|
2871
3102
|
DEFAULT_FEE_ESTIMATOR,
|
|
3103
|
+
DEFAULT_MIN_CONF,
|
|
2872
3104
|
DID_REGEX,
|
|
2873
3105
|
DUST_LIMIT_SATS,
|
|
2874
3106
|
DidBtcr2,
|