@did-btcr2/method 0.59.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/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +3 -3
- package/dist/browser.mjs +3 -3
- package/dist/cjs/index.js +917 -770
- package/dist/esm/core/identifier.js +175 -23
- package/dist/esm/core/identifier.js.map +1 -1
- package/dist/types/core/identifier.d.ts +67 -0
- package/dist/types/core/identifier.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/core/identifier.ts +267 -22
package/dist/cjs/index.js
CHANGED
|
@@ -711,13 +711,13 @@ var BeaconFactory = class {
|
|
|
711
711
|
};
|
|
712
712
|
|
|
713
713
|
// src/core/beacon/signal-discovery.ts
|
|
714
|
-
var
|
|
715
|
-
var
|
|
714
|
+
var import_bitcoin4 = require("@did-btcr2/bitcoin");
|
|
715
|
+
var import_common10 = require("@did-btcr2/common");
|
|
716
716
|
|
|
717
717
|
// src/core/beacon/utils.ts
|
|
718
|
-
var
|
|
719
|
-
var
|
|
720
|
-
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");
|
|
721
721
|
|
|
722
722
|
// src/utils/appendix.ts
|
|
723
723
|
var import_dids = require("@web5/dids");
|
|
@@ -899,901 +899,1048 @@ var Appendix = class _Appendix {
|
|
|
899
899
|
};
|
|
900
900
|
|
|
901
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");
|
|
902
909
|
var import_common7 = require("@did-btcr2/common");
|
|
903
910
|
var import_keypair = require("@did-btcr2/keypair");
|
|
904
|
-
var
|
|
905
|
-
var
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
* @returns {string} The new did:btcr2 identifier.
|
|
919
|
-
*/
|
|
920
|
-
static encode(genesisBytes, options) {
|
|
921
|
-
const { idType, version = 1, network = "bitcoin" } = options;
|
|
922
|
-
if (!(idType in import_common7.IdentifierTypes)) {
|
|
923
|
-
throw new import_common7.IdentifierError('Expected "idType" to be "KEY" or "EXTERNAL"', import_common7.INVALID_DID, { idType });
|
|
924
|
-
}
|
|
925
|
-
if (version !== 1) {
|
|
926
|
-
throw new import_common7.IdentifierError('Expected "version" to be 1', import_common7.INVALID_DID, { version });
|
|
927
|
-
}
|
|
928
|
-
if (typeof network !== "string") {
|
|
929
|
-
throw new import_common7.IdentifierError('Expected "network" to be a known network name', import_common7.INVALID_DID, { network });
|
|
930
|
-
}
|
|
931
|
-
const networkValue = import_common7.BitcoinNetworkNames[network];
|
|
932
|
-
if (networkValue === void 0) {
|
|
933
|
-
throw new import_common7.IdentifierError('Invalid "network" name', import_common7.INVALID_DID, { network });
|
|
934
|
-
}
|
|
935
|
-
if (idType === "KEY") {
|
|
936
|
-
try {
|
|
937
|
-
new import_keypair.CompressedSecp256k1PublicKey(genesisBytes);
|
|
938
|
-
} catch {
|
|
939
|
-
throw new import_common7.IdentifierError(
|
|
940
|
-
'Expected "genesisBytes" to be a valid compressed secp256k1 public key',
|
|
941
|
-
import_common7.INVALID_DID,
|
|
942
|
-
{ genesisBytes }
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
} else if (genesisBytes.length !== 32) {
|
|
946
|
-
throw new import_common7.IdentifierError(
|
|
947
|
-
'Expected "genesisBytes" to be a 32-byte hash for EXTERNAL identifiers',
|
|
948
|
-
import_common7.INVALID_DID,
|
|
949
|
-
{ genesisBytes }
|
|
950
|
-
);
|
|
951
|
-
}
|
|
952
|
-
const hrp = idType === "KEY" ? "k" : "x";
|
|
953
|
-
const firstByte = version - 1 << 4 | networkValue;
|
|
954
|
-
const dataBytes = new Uint8Array([firstByte, ...genesisBytes]);
|
|
955
|
-
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;
|
|
956
925
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
if (
|
|
968
|
-
throw new import_common7.
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}
|
|
974
|
-
if (method !== "btcr2") {
|
|
975
|
-
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
|
+
);
|
|
976
942
|
}
|
|
977
|
-
if (!
|
|
978
|
-
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
|
+
);
|
|
979
949
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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;
|
|
983
957
|
}
|
|
984
|
-
|
|
985
|
-
|
|
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);
|
|
986
977
|
}
|
|
987
|
-
const idType =
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
|
|
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
|
+
}
|
|
991
991
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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];
|
|
1000
1005
|
} else {
|
|
1001
|
-
|
|
1006
|
+
this.authentication = document.authentication;
|
|
1007
|
+
this.assertionMethod = document.assertionMethod;
|
|
1008
|
+
this.capabilityInvocation = document.capabilityInvocation;
|
|
1009
|
+
this.capabilityDelegation = document.capabilityDelegation;
|
|
1002
1010
|
}
|
|
1003
|
-
|
|
1004
|
-
if (
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
throw new import_common7.IdentifierError(`Invalid genesisBytes: ${genesisBytes}`, import_common7.INVALID_DID, { identifier });
|
|
1009
|
-
}
|
|
1010
|
-
} else if (genesisBytes.length !== 32) {
|
|
1011
|
-
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);
|
|
1012
1016
|
}
|
|
1013
|
-
return { idType, hrp, version, network, genesisBytes };
|
|
1014
1017
|
}
|
|
1015
1018
|
/**
|
|
1016
|
-
*
|
|
1017
|
-
* @returns {
|
|
1019
|
+
* Convert the DidDocument to a JSON object.
|
|
1020
|
+
* @returns {DidDocument} The JSON representation of the DidDocument.
|
|
1018
1021
|
*/
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
+
};
|
|
1030
1034
|
}
|
|
1031
1035
|
/**
|
|
1032
|
-
*
|
|
1033
|
-
* @param {string}
|
|
1034
|
-
* @
|
|
1035
|
-
* @
|
|
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.
|
|
1036
1040
|
*/
|
|
1037
|
-
static
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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);
|
|
1047
1056
|
}
|
|
1048
1057
|
/**
|
|
1049
|
-
*
|
|
1050
|
-
* @param {
|
|
1051
|
-
* @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.
|
|
1052
1061
|
*/
|
|
1053
|
-
static
|
|
1054
|
-
|
|
1055
|
-
this.decode(identifier);
|
|
1056
|
-
return true;
|
|
1057
|
-
} catch {
|
|
1058
|
-
return false;
|
|
1059
|
-
}
|
|
1062
|
+
static fromExternalIdentifier(data) {
|
|
1063
|
+
return new _DidDocument(data);
|
|
1060
1064
|
}
|
|
1061
|
-
};
|
|
1062
|
-
|
|
1063
|
-
// src/core/beacon/utils.ts
|
|
1064
|
-
var BeaconUtils = class {
|
|
1065
1065
|
/**
|
|
1066
|
-
*
|
|
1067
|
-
* @
|
|
1068
|
-
* @returns {string} The Bitcoin address extracted from the URI
|
|
1069
|
-
* @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
|
|
1070
1068
|
*/
|
|
1071
|
-
static
|
|
1072
|
-
|
|
1073
|
-
|
|
1069
|
+
static sanitize(doc) {
|
|
1070
|
+
for (const key of Object.keys(doc)) {
|
|
1071
|
+
if (doc[key] === void 0) {
|
|
1072
|
+
delete doc[key];
|
|
1073
|
+
}
|
|
1074
1074
|
}
|
|
1075
|
-
return
|
|
1075
|
+
return doc;
|
|
1076
1076
|
}
|
|
1077
1077
|
/**
|
|
1078
|
-
* Validates
|
|
1079
|
-
* @param {
|
|
1080
|
-
* @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.
|
|
1081
1082
|
*/
|
|
1082
|
-
static
|
|
1083
|
-
if (!
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
+
}
|
|
1086
1099
|
return true;
|
|
1087
1100
|
}
|
|
1088
1101
|
/**
|
|
1089
|
-
*
|
|
1090
|
-
* @
|
|
1091
|
-
* @
|
|
1092
|
-
* @
|
|
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.
|
|
1093
1106
|
*/
|
|
1094
|
-
static
|
|
1095
|
-
|
|
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));
|
|
1096
1110
|
}
|
|
1097
1111
|
/**
|
|
1098
|
-
*
|
|
1099
|
-
* @
|
|
1100
|
-
* @
|
|
1101
|
-
* @
|
|
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.
|
|
1102
1116
|
*/
|
|
1103
|
-
static
|
|
1117
|
+
static isValidId(id) {
|
|
1118
|
+
if (typeof id !== "string") return false;
|
|
1104
1119
|
try {
|
|
1105
|
-
|
|
1106
|
-
return
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
} catch (error) {
|
|
1110
|
-
throw new BeaconError(
|
|
1111
|
-
"Failed to create beacon services: " + error.message,
|
|
1112
|
-
"BEACON_SERVICE_ERROR",
|
|
1113
|
-
{ did, beaconType }
|
|
1114
|
-
);
|
|
1120
|
+
Identifier.decode(id);
|
|
1121
|
+
return true;
|
|
1122
|
+
} catch {
|
|
1123
|
+
return false;
|
|
1115
1124
|
}
|
|
1116
1125
|
}
|
|
1117
1126
|
/**
|
|
1118
|
-
*
|
|
1119
|
-
* @
|
|
1120
|
-
* @param {
|
|
1121
|
-
* @
|
|
1122
|
-
* @returns {BeaconService} A BeaconService object.
|
|
1123
|
-
* @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.
|
|
1124
1131
|
*/
|
|
1125
|
-
static
|
|
1126
|
-
|
|
1127
|
-
const components = Identifier.decode(did);
|
|
1128
|
-
const network = (0, import_bitcoin2.getNetwork)(components.network);
|
|
1129
|
-
const pubkey = components.genesisBytes;
|
|
1130
|
-
const id = `${did}#initial${addressType.toUpperCase()}`;
|
|
1131
|
-
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;
|
|
1132
|
-
const serviceEndpoint = `bitcoin:${address}`;
|
|
1133
|
-
return { id, type: beaconType, serviceEndpoint };
|
|
1134
|
-
} catch (error) {
|
|
1135
|
-
throw new BeaconError(
|
|
1136
|
-
"Failed to create beacon service: " + error.message,
|
|
1137
|
-
"BEACON_SERVICE_ERROR",
|
|
1138
|
-
{ did, beaconType }
|
|
1139
|
-
);
|
|
1140
|
-
}
|
|
1132
|
+
static isValidVerificationMethods(verificationMethod) {
|
|
1133
|
+
return Array.isArray(verificationMethod) && verificationMethod.every(isMultikeyVerificationMethod);
|
|
1141
1134
|
}
|
|
1142
1135
|
/**
|
|
1143
|
-
*
|
|
1144
|
-
* @
|
|
1145
|
-
* @
|
|
1146
|
-
* @
|
|
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.
|
|
1147
1140
|
*/
|
|
1148
|
-
static
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
serviceEndpoint: `bitcoin:${p2trAddr}`
|
|
1171
|
-
}
|
|
1172
|
-
];
|
|
1173
|
-
} catch (error) {
|
|
1174
|
-
throw new BeaconError(
|
|
1175
|
-
"Failed to create beacon services: " + error.message,
|
|
1176
|
-
"BEACON_SERVICE_ERROR",
|
|
1177
|
-
{ 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)
|
|
1178
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);
|
|
1179
1176
|
}
|
|
1177
|
+
return didDocument;
|
|
1180
1178
|
}
|
|
1181
1179
|
/**
|
|
1182
|
-
*
|
|
1183
|
-
* @
|
|
1184
|
-
* @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.
|
|
1185
1182
|
*/
|
|
1186
|
-
|
|
1187
|
-
|
|
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;
|
|
1188
1197
|
}
|
|
1189
1198
|
/**
|
|
1190
|
-
*
|
|
1191
|
-
* @
|
|
1192
|
-
* @returns {string[]} An array of beacon service ids.
|
|
1199
|
+
* Convert the DidDocument to an GenesisDocument.
|
|
1200
|
+
* @returns {GenesisDocument} The GenesisDocument representation of the DidDocument.
|
|
1193
1201
|
*/
|
|
1194
|
-
|
|
1195
|
-
|
|
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);
|
|
1196
1207
|
}
|
|
1197
1208
|
};
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
function extractOpReturnSignalHash(scriptPubKey) {
|
|
1202
|
-
if (!scriptPubKey) {
|
|
1203
|
-
return null;
|
|
1209
|
+
var GenesisDocument = class _GenesisDocument extends DidDocument {
|
|
1210
|
+
constructor(document) {
|
|
1211
|
+
super(document);
|
|
1204
1212
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1213
|
+
/**
|
|
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.
|
|
1217
|
+
*/
|
|
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);
|
|
1208
1222
|
}
|
|
1209
|
-
return signal[1].toLowerCase();
|
|
1210
|
-
}
|
|
1211
|
-
var BeaconSignalDiscovery = class _BeaconSignalDiscovery {
|
|
1212
1223
|
/**
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
1216
|
-
* A Beacon Signal is a transaction that *spends from* a Beacon Address, but an address
|
|
1217
|
-
* transaction listing returns every transaction touching the address in either
|
|
1218
|
-
* direction. Without this check, anyone able to pay dust to a beacon address could
|
|
1219
|
-
* attach an arbitrary 32-byte OP_RETURN and have it read as a signal, so the input side
|
|
1220
|
-
* has to be inspected before a transaction is treated as one.
|
|
1221
|
-
*
|
|
1222
|
-
* Esplora embeds the spent output in `vin[].prevout`; when a backend omits it the
|
|
1223
|
-
* funding transaction is fetched instead, so a missing field cannot silently drop a
|
|
1224
|
-
* real signal.
|
|
1225
|
-
*
|
|
1226
|
-
* @param {RawTransactionRest} tx The candidate transaction.
|
|
1227
|
-
* @param {string} address The beacon address the transaction must spend from.
|
|
1228
|
-
* @param {BitcoinConnection} bitcoin Bitcoin network connection to use for REST calls.
|
|
1229
|
-
* @returns {Promise<boolean>} True if at least one input spends an output of the beacon address.
|
|
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.
|
|
1230
1227
|
*/
|
|
1231
|
-
static
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
continue;
|
|
1235
|
-
}
|
|
1236
|
-
let prevout = vin.prevout;
|
|
1237
|
-
if (!prevout && vin.txid) {
|
|
1238
|
-
const fundingTx = await bitcoin.rest.transaction.get(vin.txid);
|
|
1239
|
-
prevout = fundingTx?.vout?.[vin.vout];
|
|
1240
|
-
}
|
|
1241
|
-
if (prevout?.scriptpubkey_address === address) {
|
|
1242
|
-
return true;
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
return false;
|
|
1228
|
+
static fromDidDocument(didDocument) {
|
|
1229
|
+
const intermediateDocument = import_common7.JSONUtils.cloneReplace(didDocument, DID_REGEX, ID_PLACEHOLDER_VALUE);
|
|
1230
|
+
return new _GenesisDocument(intermediateDocument);
|
|
1246
1231
|
}
|
|
1247
1232
|
/**
|
|
1248
|
-
*
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
1251
|
-
*
|
|
1252
|
-
*
|
|
1253
|
-
* has no block height and no block time, so it cannot carry block metadata.
|
|
1254
|
-
* The specification also says that a resolver must not process an
|
|
1255
|
-
* unconfirmed transaction. An absent flag counts as unconfirmed, as it does
|
|
1256
|
-
* for UTXO selection. The check runs before the OP_RETURN parse, so a
|
|
1257
|
-
* mempool transaction costs no prevout fetch. The {@link fullnode} path
|
|
1258
|
-
* needs no such check: it walks mined blocks only.
|
|
1259
|
-
*
|
|
1260
|
-
* The `confirmations` count uses the block count fetched before the listing.
|
|
1261
|
-
* A block that arrives between the two calls yields a count of `0` for its
|
|
1262
|
-
* transactions. The resolver then excludes them, because its minimum is at
|
|
1263
|
-
* least `1`. An under-count is the safe direction, so keep that order.
|
|
1264
|
-
* @param {Array<BeaconService>} beaconServices Array of BeaconService objects to retrieve signals for
|
|
1265
|
-
* @param {BitcoinConnection} bitcoin Bitcoin network connection to use for REST calls
|
|
1266
|
-
* @returns {Promise<Map<BeaconService, Array<BeaconSignal>>>} Map of beacon service to its discovered signals
|
|
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.
|
|
1267
1238
|
*/
|
|
1268
|
-
static
|
|
1269
|
-
|
|
1270
|
-
const currentBlockCount = await bitcoin.rest.block.count();
|
|
1271
|
-
for (const beaconService of beaconServices) {
|
|
1272
|
-
beaconServiceSignals.set(beaconService, []);
|
|
1273
|
-
const beaconAddress = BeaconUtils.parseBitcoinAddress(beaconService.serviceEndpoint);
|
|
1274
|
-
const beaconSignals = await bitcoin.rest.address.getTxs(beaconAddress);
|
|
1275
|
-
if (!beaconSignals || !beaconSignals.length) {
|
|
1276
|
-
continue;
|
|
1277
|
-
}
|
|
1278
|
-
for (const beaconSignal of beaconSignals) {
|
|
1279
|
-
const status = beaconSignal.status;
|
|
1280
|
-
if (status.confirmed !== true) {
|
|
1281
|
-
continue;
|
|
1282
|
-
}
|
|
1283
|
-
const lastSignalVout = beaconSignal.vout.slice(-1)[0];
|
|
1284
|
-
if (!lastSignalVout) {
|
|
1285
|
-
continue;
|
|
1286
|
-
}
|
|
1287
|
-
const updateHash = extractOpReturnSignalHash(lastSignalVout.scriptpubkey);
|
|
1288
|
-
if (!updateHash) {
|
|
1289
|
-
continue;
|
|
1290
|
-
}
|
|
1291
|
-
if (!await _BeaconSignalDiscovery.spendsFromAddress(beaconSignal, beaconAddress, bitcoin)) {
|
|
1292
|
-
continue;
|
|
1293
|
-
}
|
|
1294
|
-
const confirmations = currentBlockCount - status.block_height + 1;
|
|
1295
|
-
beaconServiceSignals.get(beaconService)?.push({
|
|
1296
|
-
tx: beaconSignal,
|
|
1297
|
-
signalBytes: updateHash,
|
|
1298
|
-
blockMetadata: {
|
|
1299
|
-
confirmations,
|
|
1300
|
-
height: status.block_height,
|
|
1301
|
-
time: status.block_time
|
|
1302
|
-
}
|
|
1303
|
-
});
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
return beaconServiceSignals;
|
|
1239
|
+
static create(verificationMethod, relationships, service) {
|
|
1240
|
+
return new _GenesisDocument({ id: ID_PLACEHOLDER_VALUE, ...relationships, verificationMethod, service });
|
|
1307
1241
|
}
|
|
1308
1242
|
/**
|
|
1309
|
-
*
|
|
1310
|
-
* @param {
|
|
1311
|
-
* @
|
|
1312
|
-
* @returns {Promise<Map<BeaconService, Array<BeaconSignal>>>} Map of beacon service to its discovered signals.
|
|
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.
|
|
1313
1246
|
*/
|
|
1314
|
-
static
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
}
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
const lastSignalVout = tx.vout.slice(-1)[0];
|
|
1336
|
-
if (!lastSignalVout) {
|
|
1337
|
-
continue;
|
|
1338
|
-
}
|
|
1339
|
-
const updateHash = extractOpReturnSignalHash(lastSignalVout.scriptPubKey?.hex);
|
|
1340
|
-
if (!updateHash) {
|
|
1341
|
-
continue;
|
|
1342
|
-
}
|
|
1343
|
-
const signaled = /* @__PURE__ */ new Set();
|
|
1344
|
-
for (const vin of tx.vin) {
|
|
1345
|
-
if (vin.coinbase) {
|
|
1346
|
-
continue;
|
|
1347
|
-
}
|
|
1348
|
-
if (vin.txinwitness && vin.txinwitness.length === 1 && vin.txinwitness[0] === import_bitcoin3.TXIN_WITNESS_COINBASE) {
|
|
1349
|
-
continue;
|
|
1350
|
-
}
|
|
1351
|
-
if (!vin.txid) {
|
|
1352
|
-
continue;
|
|
1353
|
-
}
|
|
1354
|
-
if (vin.vout === void 0) {
|
|
1355
|
-
continue;
|
|
1356
|
-
}
|
|
1357
|
-
const prevout = await rpc.getRawTransaction(vin.txid, 2);
|
|
1358
|
-
if (!prevout.vout[vin.vout]) {
|
|
1359
|
-
continue;
|
|
1360
|
-
}
|
|
1361
|
-
const scriptPubKey = prevout.vout[vin.vout].scriptPubKey;
|
|
1362
|
-
if (!scriptPubKey.address) {
|
|
1363
|
-
continue;
|
|
1364
|
-
}
|
|
1365
|
-
const beaconService = beaconServicesMap.get(scriptPubKey.address);
|
|
1366
|
-
if (!beaconService || signaled.has(beaconService)) {
|
|
1367
|
-
continue;
|
|
1368
|
-
}
|
|
1369
|
-
signaled.add(beaconService);
|
|
1370
|
-
console.info(`Tx ${tx.txid} contains beacon address ${scriptPubKey.address}`);
|
|
1371
|
-
beaconServiceSignals.get(beaconService)?.push({
|
|
1372
|
-
tx,
|
|
1373
|
-
signalBytes: updateHash,
|
|
1374
|
-
blockMetadata: {
|
|
1375
|
-
height: block.height,
|
|
1376
|
-
time: block.time,
|
|
1377
|
-
confirmations: block.confirmations
|
|
1378
|
-
}
|
|
1379
|
-
});
|
|
1380
|
-
}
|
|
1381
|
-
;
|
|
1382
|
-
}
|
|
1383
|
-
height += 1;
|
|
1384
|
-
if (height > targetHeight) {
|
|
1385
|
-
console.info(`Chain tip reached ${height}, breaking ...`);
|
|
1386
|
-
break;
|
|
1387
|
-
}
|
|
1388
|
-
block = await rpc.getBlock({ height });
|
|
1389
|
-
}
|
|
1390
|
-
return beaconServiceSignals;
|
|
1391
|
-
}
|
|
1392
|
-
};
|
|
1393
|
-
|
|
1394
|
-
// src/core/did-sender-resolver.ts
|
|
1395
|
-
var import_common14 = require("@did-btcr2/common");
|
|
1396
|
-
var import_cryptosuite3 = require("@did-btcr2/cryptosuite");
|
|
1397
|
-
var import_keypair4 = require("@did-btcr2/keypair");
|
|
1398
|
-
|
|
1399
|
-
// src/core/resolver.ts
|
|
1400
|
-
var import_bitcoin5 = require("@did-btcr2/bitcoin");
|
|
1401
|
-
var import_common13 = require("@did-btcr2/common");
|
|
1402
|
-
var import_cryptosuite2 = require("@did-btcr2/cryptosuite");
|
|
1403
|
-
var import_keypair3 = require("@did-btcr2/keypair");
|
|
1404
|
-
|
|
1405
|
-
// src/did-btcr2.ts
|
|
1406
|
-
var import_common12 = require("@did-btcr2/common");
|
|
1407
|
-
var import_dids2 = require("@web5/dids");
|
|
1408
|
-
|
|
1409
|
-
// src/core/updater.ts
|
|
1410
|
-
var import_common11 = require("@did-btcr2/common");
|
|
1411
|
-
var import_cryptosuite = require("@did-btcr2/cryptosuite");
|
|
1412
|
-
|
|
1413
|
-
// src/utils/did-document.ts
|
|
1414
|
-
var import_bitcoin4 = require("@did-btcr2/bitcoin");
|
|
1415
|
-
var import_common10 = require("@did-btcr2/common");
|
|
1416
|
-
var import_keypair2 = require("@did-btcr2/keypair");
|
|
1417
|
-
var import_utils4 = require("@web5/dids/utils");
|
|
1418
|
-
var import_btc_signer3 = require("@scure/btc-signer");
|
|
1419
|
-
var BTCR2_DID_DOCUMENT_CONTEXT = [
|
|
1420
|
-
"https://www.w3.org/ns/did/v1.1",
|
|
1421
|
-
"https://btcr2.dev/context/v1"
|
|
1422
|
-
];
|
|
1423
|
-
var MULTIKEY_VERIFICATION_METHOD_TYPE = "Multikey";
|
|
1424
|
-
var MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX = "zQ3s";
|
|
1425
|
-
var ID_PLACEHOLDER_VALUE = "did:btcr2:_";
|
|
1426
|
-
var BECH32M_CHARS = "";
|
|
1427
|
-
var DID_REGEX = /did:btcr2:(x1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]*)/g;
|
|
1428
|
-
function isMultikeyVerificationMethod(vm) {
|
|
1429
|
-
if (!Appendix.isDidVerificationMethod(vm)) {
|
|
1430
|
-
return false;
|
|
1431
|
-
}
|
|
1432
|
-
const { type, publicKeyMultibase } = vm;
|
|
1433
|
-
return type === MULTIKEY_VERIFICATION_METHOD_TYPE && typeof publicKeyMultibase === "string" && publicKeyMultibase.startsWith(MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX);
|
|
1434
|
-
}
|
|
1435
|
-
var DidVerificationMethod = class {
|
|
1436
|
-
id;
|
|
1437
|
-
type;
|
|
1438
|
-
controller;
|
|
1439
|
-
publicKeyMultibase;
|
|
1440
|
-
secretKeyMultibase;
|
|
1441
|
-
constructor({ id, type, controller, publicKeyMultibase, secretKeyMultibase }) {
|
|
1442
|
-
if (type !== MULTIKEY_VERIFICATION_METHOD_TYPE) {
|
|
1443
|
-
throw new import_common10.DidDocumentError(
|
|
1444
|
-
`Invalid verification method: type must be "${MULTIKEY_VERIFICATION_METHOD_TYPE}"`,
|
|
1445
|
-
import_common10.INVALID_DID_DOCUMENT,
|
|
1446
|
-
{ id, type }
|
|
1447
|
-
);
|
|
1448
|
-
}
|
|
1449
|
-
if (typeof publicKeyMultibase !== "string" || !publicKeyMultibase.startsWith(MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX)) {
|
|
1450
|
-
throw new import_common10.DidDocumentError(
|
|
1451
|
-
`Invalid verification method: publicKeyMultibase must start with "${MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX}"`,
|
|
1452
|
-
import_common10.INVALID_DID_DOCUMENT,
|
|
1453
|
-
{ id, publicKeyMultibase }
|
|
1454
|
-
);
|
|
1455
|
-
}
|
|
1456
|
-
this.id = id;
|
|
1457
|
-
this.type = type;
|
|
1458
|
-
this.controller = controller;
|
|
1459
|
-
this.publicKeyMultibase = publicKeyMultibase;
|
|
1460
|
-
this.secretKeyMultibase = secretKeyMultibase;
|
|
1461
|
-
if (!secretKeyMultibase) {
|
|
1462
|
-
delete this.secretKeyMultibase;
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
// TODO: Add helper methods and properties
|
|
1466
|
-
};
|
|
1467
|
-
var DidDocument = class _DidDocument {
|
|
1468
|
-
id;
|
|
1469
|
-
"@context" = [
|
|
1470
|
-
"https://www.w3.org/ns/did/v1.1",
|
|
1471
|
-
"https://btcr2.dev/context/v1"
|
|
1472
|
-
];
|
|
1473
|
-
verificationMethod;
|
|
1474
|
-
authentication;
|
|
1475
|
-
assertionMethod;
|
|
1476
|
-
capabilityInvocation;
|
|
1477
|
-
capabilityDelegation;
|
|
1478
|
-
service;
|
|
1479
|
-
deactivated;
|
|
1480
|
-
constructor(document) {
|
|
1481
|
-
if (!document.id) {
|
|
1482
|
-
throw new import_common10.DidDocumentError("DID Document must have an id", import_common10.INVALID_DID_DOCUMENT, document);
|
|
1483
|
-
}
|
|
1484
|
-
const idType = document.id.includes("k1") ? import_common10.IdentifierTypes.KEY : import_common10.IdentifierTypes.EXTERNAL;
|
|
1485
|
-
const isGenesis = document.id === ID_PLACEHOLDER_VALUE;
|
|
1486
|
-
const { id, verificationMethod: vm, service } = document;
|
|
1487
|
-
if (!isGenesis) {
|
|
1488
|
-
if (!_DidDocument.isValidId(id)) {
|
|
1489
|
-
throw new import_common10.DidDocumentError(`Invalid id: ${id}`, import_common10.INVALID_DID_DOCUMENT, document);
|
|
1490
|
-
}
|
|
1491
|
-
if (!_DidDocument.isValidVerificationMethods(vm)) {
|
|
1492
|
-
throw new import_common10.DidDocumentError("Invalid verificationMethod: " + vm, import_common10.INVALID_DID_DOCUMENT, document);
|
|
1493
|
-
}
|
|
1494
|
-
if (!_DidDocument.isValidServices(service)) {
|
|
1495
|
-
throw new import_common10.DidDocumentError("Invalid service: " + service, import_common10.INVALID_DID_DOCUMENT, document);
|
|
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
|
|
1496
1268
|
}
|
|
1497
|
-
}
|
|
1498
|
-
this.id = document.id;
|
|
1499
|
-
this.verificationMethod = document.verificationMethod || [];
|
|
1500
|
-
this.service = document.service || [];
|
|
1501
|
-
this["@context"] = document["@context"] || [
|
|
1502
|
-
"https://www.w3.org/ns/did/v1.1",
|
|
1503
|
-
"https://btcr2.dev/context/v1"
|
|
1504
1269
|
];
|
|
1505
|
-
|
|
1506
|
-
const keyRef = `${this.id}#initialKey`;
|
|
1507
|
-
this.authentication = document.authentication || [keyRef];
|
|
1508
|
-
this.assertionMethod = document.assertionMethod || [keyRef];
|
|
1509
|
-
this.capabilityInvocation = document.capabilityInvocation || [keyRef];
|
|
1510
|
-
this.capabilityDelegation = document.capabilityDelegation || [keyRef];
|
|
1511
|
-
} else {
|
|
1512
|
-
this.authentication = document.authentication;
|
|
1513
|
-
this.assertionMethod = document.assertionMethod;
|
|
1514
|
-
this.capabilityInvocation = document.capabilityInvocation;
|
|
1515
|
-
this.capabilityDelegation = document.capabilityDelegation;
|
|
1516
|
-
}
|
|
1517
|
-
_DidDocument.sanitize(this);
|
|
1518
|
-
if (isGenesis) {
|
|
1519
|
-
this.validateGenesis();
|
|
1520
|
-
} else {
|
|
1521
|
-
_DidDocument.validate(this);
|
|
1522
|
-
}
|
|
1270
|
+
return _GenesisDocument.create(verificationMethod, relationships, services);
|
|
1523
1271
|
}
|
|
1524
1272
|
/**
|
|
1525
|
-
*
|
|
1526
|
-
* @
|
|
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.
|
|
1527
1276
|
*/
|
|
1528
|
-
|
|
1529
|
-
return
|
|
1530
|
-
id: this.id,
|
|
1531
|
-
"@context": this["@context"],
|
|
1532
|
-
verificationMethod: this.verificationMethod,
|
|
1533
|
-
authentication: this.authentication,
|
|
1534
|
-
assertionMethod: this.assertionMethod,
|
|
1535
|
-
capabilityInvocation: this.capabilityInvocation,
|
|
1536
|
-
capabilityDelegation: this.capabilityDelegation,
|
|
1537
|
-
service: this.service,
|
|
1538
|
-
deactivated: this.deactivated
|
|
1539
|
-
};
|
|
1277
|
+
static fromJSON(object) {
|
|
1278
|
+
return new _GenesisDocument(object);
|
|
1540
1279
|
}
|
|
1541
1280
|
/**
|
|
1542
|
-
*
|
|
1543
|
-
* @param {
|
|
1544
|
-
* @
|
|
1545
|
-
* @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.
|
|
1546
1284
|
*/
|
|
1547
|
-
static
|
|
1548
|
-
|
|
1549
|
-
const document = {
|
|
1550
|
-
id,
|
|
1551
|
-
verificationMethod: [
|
|
1552
|
-
new DidVerificationMethod({
|
|
1553
|
-
id,
|
|
1554
|
-
type: "Multikey",
|
|
1555
|
-
controller: id,
|
|
1556
|
-
publicKeyMultibase
|
|
1557
|
-
})
|
|
1558
|
-
],
|
|
1559
|
-
service
|
|
1560
|
-
};
|
|
1561
|
-
return new _DidDocument(document);
|
|
1285
|
+
static toGenesisBytes(genesisDocument) {
|
|
1286
|
+
return (0, import_common7.hash)((0, import_common7.canonicalize)(genesisDocument));
|
|
1562
1287
|
}
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
// src/core/identifier.ts
|
|
1291
|
+
var DID_PREFIX = "did:btcr2:";
|
|
1292
|
+
var Identifier = class _Identifier {
|
|
1563
1293
|
/**
|
|
1564
|
-
*
|
|
1565
|
-
*
|
|
1566
|
-
*
|
|
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.
|
|
1567
1306
|
*/
|
|
1568
|
-
static
|
|
1569
|
-
|
|
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)}`;
|
|
1570
1343
|
}
|
|
1571
1344
|
/**
|
|
1572
|
-
*
|
|
1573
|
-
* @
|
|
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
|
|
1574
1351
|
*/
|
|
1575
|
-
static
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
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 });
|
|
1579
1395
|
}
|
|
1396
|
+
} else if (genesisBytes.length !== 32) {
|
|
1397
|
+
throw new import_common8.IdentifierError(`Invalid genesisBytes: ${genesisBytes}`, import_common8.INVALID_DID, { identifier });
|
|
1580
1398
|
}
|
|
1581
|
-
return
|
|
1582
|
-
}
|
|
1583
|
-
/**
|
|
1584
|
-
* Validates a
|
|
1585
|
-
* @
|
|
1586
|
-
*
|
|
1587
|
-
*
|
|
1399
|
+
return { idType, hrp, version, network, genesisBytes };
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
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.
|
|
1588
1419
|
*/
|
|
1589
|
-
static
|
|
1590
|
-
|
|
1591
|
-
|
|
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.");
|
|
1592
1431
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
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.`);
|
|
1595
1435
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1436
|
+
const encoded = parts[2];
|
|
1437
|
+
if (encoded.length === 0) {
|
|
1438
|
+
return fail("prefix", "The method-specific id is empty.");
|
|
1598
1439
|
}
|
|
1599
|
-
|
|
1600
|
-
|
|
1440
|
+
pass("prefix");
|
|
1441
|
+
if (encoded !== encoded.toLowerCase()) {
|
|
1442
|
+
return fail("lowercase", "The method-specific id must be lowercase.");
|
|
1601
1443
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
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)}`);
|
|
1604
1451
|
}
|
|
1605
|
-
|
|
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 };
|
|
1606
1546
|
}
|
|
1607
1547
|
/**
|
|
1608
|
-
*
|
|
1609
|
-
* @
|
|
1610
|
-
* @param {DidDocument['@context']} context The context to validate.
|
|
1611
|
-
* @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.
|
|
1612
1550
|
*/
|
|
1613
|
-
static
|
|
1614
|
-
|
|
1615
|
-
|
|
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 };
|
|
1616
1562
|
}
|
|
1617
1563
|
/**
|
|
1618
|
-
*
|
|
1619
|
-
* @
|
|
1620
|
-
* @
|
|
1621
|
-
* @
|
|
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).
|
|
1622
1568
|
*/
|
|
1623
|
-
static
|
|
1624
|
-
|
|
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) {
|
|
1625
1586
|
try {
|
|
1626
|
-
|
|
1587
|
+
this.decode(identifier);
|
|
1627
1588
|
return true;
|
|
1628
1589
|
} catch {
|
|
1629
1590
|
return false;
|
|
1630
1591
|
}
|
|
1631
1592
|
}
|
|
1593
|
+
};
|
|
1594
|
+
|
|
1595
|
+
// src/core/beacon/utils.ts
|
|
1596
|
+
var BeaconUtils = class {
|
|
1632
1597
|
/**
|
|
1633
|
-
*
|
|
1634
|
-
* @
|
|
1635
|
-
* @
|
|
1636
|
-
* @
|
|
1637
|
-
*/
|
|
1638
|
-
static isValidVerificationMethods(verificationMethod) {
|
|
1639
|
-
return Array.isArray(verificationMethod) && verificationMethod.every(isMultikeyVerificationMethod);
|
|
1640
|
-
}
|
|
1641
|
-
/**
|
|
1642
|
-
* Validates that the DID Document has valid services.
|
|
1643
|
-
* @private
|
|
1644
|
-
* @param {DidService[]} service The services to validate.
|
|
1645
|
-
* @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
|
|
1646
1602
|
*/
|
|
1647
|
-
static
|
|
1648
|
-
|
|
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];
|
|
1649
1608
|
}
|
|
1650
1609
|
/**
|
|
1651
|
-
* Validates
|
|
1652
|
-
* @
|
|
1653
|
-
* @
|
|
1654
|
-
* @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
|
|
1655
1613
|
*/
|
|
1656
|
-
static
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
"capabilityDelegation"
|
|
1662
|
-
];
|
|
1663
|
-
const keys = Object.keys(didDocument);
|
|
1664
|
-
const availableKeys = possibleVerificationRelationships.filter((key) => keys.includes(key));
|
|
1665
|
-
return availableKeys.every((key) => {
|
|
1666
|
-
const value = didDocument[key];
|
|
1667
|
-
return value && Array.isArray(value) && value.every(
|
|
1668
|
-
(entry) => typeof entry === "string" || Appendix.isDidVerificationMethod(entry)
|
|
1669
|
-
);
|
|
1670
|
-
});
|
|
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;
|
|
1671
1619
|
}
|
|
1672
1620
|
/**
|
|
1673
|
-
*
|
|
1674
|
-
* @
|
|
1675
|
-
* @
|
|
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
|
|
1676
1625
|
*/
|
|
1677
|
-
static
|
|
1678
|
-
|
|
1679
|
-
didDocument.validateGenesis();
|
|
1680
|
-
} else {
|
|
1681
|
-
_DidDocument.isValid(didDocument);
|
|
1682
|
-
}
|
|
1683
|
-
return didDocument;
|
|
1626
|
+
static getBeaconServices(didDocument) {
|
|
1627
|
+
return didDocument.service.filter(this.isBeaconService) ?? [];
|
|
1684
1628
|
}
|
|
1685
1629
|
/**
|
|
1686
|
-
*
|
|
1687
|
-
* @
|
|
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.
|
|
1688
1634
|
*/
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
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
|
+
);
|
|
1701
1647
|
}
|
|
1702
|
-
return true;
|
|
1703
1648
|
}
|
|
1704
1649
|
/**
|
|
1705
|
-
*
|
|
1706
|
-
* @
|
|
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.
|
|
1707
1656
|
*/
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
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
|
+
);
|
|
1711
1672
|
}
|
|
1712
|
-
return new GenesisDocument(this);
|
|
1713
|
-
}
|
|
1714
|
-
};
|
|
1715
|
-
var GenesisDocument = class _GenesisDocument extends DidDocument {
|
|
1716
|
-
constructor(document) {
|
|
1717
|
-
super(document);
|
|
1718
1673
|
}
|
|
1719
1674
|
/**
|
|
1720
|
-
*
|
|
1721
|
-
* @param did The DID to
|
|
1722
|
-
* @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.
|
|
1723
1679
|
*/
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
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
|
+
}
|
|
1728
1712
|
}
|
|
1729
1713
|
/**
|
|
1730
|
-
*
|
|
1731
|
-
* @param {
|
|
1732
|
-
* @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.
|
|
1733
1717
|
*/
|
|
1734
|
-
static
|
|
1735
|
-
|
|
1736
|
-
return new _GenesisDocument(intermediateDocument);
|
|
1718
|
+
static parseBeaconServiceEndpoint(beacon) {
|
|
1719
|
+
return { ...beacon, serviceEndpoint: beacon.serviceEndpoint.replace("bitcoin:", "") };
|
|
1737
1720
|
}
|
|
1738
1721
|
/**
|
|
1739
|
-
*
|
|
1740
|
-
* @param {
|
|
1741
|
-
* @
|
|
1742
|
-
* @param {Array<BeaconService>} service The service to be included in the document.
|
|
1743
|
-
* @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.
|
|
1744
1725
|
*/
|
|
1745
|
-
static
|
|
1746
|
-
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;
|
|
1747
1736
|
}
|
|
1737
|
+
const signal = BEACON_SIGNAL_SCRIPT.exec(scriptPubKey.trim());
|
|
1738
|
+
if (!signal) {
|
|
1739
|
+
return null;
|
|
1740
|
+
}
|
|
1741
|
+
return signal[1].toLowerCase();
|
|
1742
|
+
}
|
|
1743
|
+
var BeaconSignalDiscovery = class _BeaconSignalDiscovery {
|
|
1748
1744
|
/**
|
|
1749
|
-
*
|
|
1750
|
-
*
|
|
1751
|
-
*
|
|
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.
|
|
1752
1762
|
*/
|
|
1753
|
-
static
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
const services = [{
|
|
1758
|
-
id: `${id}#service-0`,
|
|
1759
|
-
serviceEndpoint: `bitcoin:${address}`,
|
|
1760
|
-
type: "SingletonBeacon"
|
|
1761
|
-
}];
|
|
1762
|
-
const relationships = {
|
|
1763
|
-
authentication: [`${id}#key-0`],
|
|
1764
|
-
assertionMethod: [`${id}#key-0`],
|
|
1765
|
-
capabilityInvocation: [`${id}#key-0`],
|
|
1766
|
-
capabilityDelegation: [`${id}#key-0`]
|
|
1767
|
-
};
|
|
1768
|
-
const verificationMethod = [
|
|
1769
|
-
{
|
|
1770
|
-
id: `${id}#key-0`,
|
|
1771
|
-
type: "Multikey",
|
|
1772
|
-
controller: id,
|
|
1773
|
-
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;
|
|
1774
1767
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
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;
|
|
1777
1778
|
}
|
|
1778
1779
|
/**
|
|
1779
|
-
*
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
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
|
|
1782
1799
|
*/
|
|
1783
|
-
static
|
|
1784
|
-
|
|
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;
|
|
1785
1839
|
}
|
|
1786
1840
|
/**
|
|
1787
|
-
*
|
|
1788
|
-
* @param {
|
|
1789
|
-
* @
|
|
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.
|
|
1790
1845
|
*/
|
|
1791
|
-
static
|
|
1792
|
-
|
|
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;
|
|
1793
1923
|
}
|
|
1794
1924
|
};
|
|
1795
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
|
+
|
|
1796
1941
|
// src/core/updater.ts
|
|
1942
|
+
var import_common11 = require("@did-btcr2/common");
|
|
1943
|
+
var import_cryptosuite = require("@did-btcr2/cryptosuite");
|
|
1797
1944
|
var Updater = class _Updater {
|
|
1798
1945
|
#state = { phase: "Construct" };
|
|
1799
1946
|
#sourceDocument;
|
|
@@ -2253,7 +2400,7 @@ var DidBtcr2 = class {
|
|
|
2253
2400
|
};
|
|
2254
2401
|
|
|
2255
2402
|
// src/core/resolver.ts
|
|
2256
|
-
var
|
|
2403
|
+
var import_utils7 = require("@noble/curves/utils.js");
|
|
2257
2404
|
var DEFAULT_MIN_CONF = 6;
|
|
2258
2405
|
function isRecord(value) {
|
|
2259
2406
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -2378,7 +2525,7 @@ var Resolver = class _Resolver {
|
|
|
2378
2525
|
*/
|
|
2379
2526
|
static external(didComponents, genesisDocument) {
|
|
2380
2527
|
const genesisDocumentHash = (0, import_common13.canonicalHashBytes)(genesisDocument);
|
|
2381
|
-
if (!(0,
|
|
2528
|
+
if (!(0, import_utils7.equalBytes)(didComponents.genesisBytes, genesisDocumentHash)) {
|
|
2382
2529
|
throw new import_common13.ResolveError(
|
|
2383
2530
|
`Initial document mismatch: genesisBytes !== genesisDocumentHash`,
|
|
2384
2531
|
import_common13.INVALID_DID_DOCUMENT,
|
|
@@ -2463,7 +2610,7 @@ var Resolver = class _Resolver {
|
|
|
2463
2610
|
}
|
|
2464
2611
|
if (update.targetVersionId === currentVersionId + 1) {
|
|
2465
2612
|
const sourceHashBytes = (0, import_common13.decode)(update.sourceHash, "base64urlnopad");
|
|
2466
|
-
if (!(0,
|
|
2613
|
+
if (!(0, import_utils7.equalBytes)(sourceHashBytes, currentDocumentHash)) {
|
|
2467
2614
|
throw new import_common13.ResolveError(
|
|
2468
2615
|
`Hash mismatch: update.sourceHash !== currentDocumentHash`,
|
|
2469
2616
|
import_common13.INVALID_DID_UPDATE,
|
|
@@ -2527,7 +2674,7 @@ var Resolver = class _Resolver {
|
|
|
2527
2674
|
}
|
|
2528
2675
|
);
|
|
2529
2676
|
}
|
|
2530
|
-
if (!(0,
|
|
2677
|
+
if (!(0, import_utils7.equalBytes)(historicalUpdateHash, unsignedUpdateHash)) {
|
|
2531
2678
|
throw new import_common13.ResolveError(
|
|
2532
2679
|
`Invalid duplicate: unsigned update hash does not match historical hash`,
|
|
2533
2680
|
import_common13.LATE_PUBLISHING_ERROR,
|
|
@@ -2594,7 +2741,7 @@ var Resolver = class _Resolver {
|
|
|
2594
2741
|
DidDocument.validate(updatedDocument);
|
|
2595
2742
|
const currentDocumentHash = (0, import_common13.canonicalHashBytes)(updatedDocument);
|
|
2596
2743
|
const updateTargetHash = (0, import_common13.decode)(update.targetHash);
|
|
2597
|
-
if (!(0,
|
|
2744
|
+
if (!(0, import_utils7.equalBytes)(updateTargetHash, currentDocumentHash)) {
|
|
2598
2745
|
throw new import_common13.ResolveError(
|
|
2599
2746
|
`Invalid update: update.targetHash !== currentDocumentHash`,
|
|
2600
2747
|
import_common13.INVALID_DID_UPDATE,
|