Merge pull request #61 from confio/pen

Add a Pen for signing
This commit is contained in:
Simon Warta 2020-02-07 11:32:05 +01:00 committed by GitHub
commit d6fe25d696
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 204 additions and 128 deletions

View File

@ -1,5 +1,3 @@
build/
dist/
docs/
selftest_userprofile_db/

View File

@ -64,9 +64,8 @@ const sendTokensMsg: types.MsgSend = {
},
};
const signBytes = makeSignBytes([sendTokensMsg], defaultFee, defaultNetworkId, memo, account) as SignableBytes;
const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256);
const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature);
const signBytes = makeSignBytes([sendTokensMsg], defaultFee, defaultNetworkId, memo, account);
const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
const signedTx: types.StdTx = {
msg: [sendTokensMsg],
fee: defaultFee,

View File

@ -12,10 +12,7 @@ const defaultFee: types.StdFee = {
const faucetMnemonic =
"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone";
const faucetPath = HdPaths.cosmos(0);
const faucetAddress = "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6";
const wallet = Secp256k1HdWallet.fromMnemonic(faucetMnemonic);
const signer = await wallet.createIdentity("unused_value" as ChainId, faucetPath);
const pen = await Secp256k1Pen.fromMnemonic(faucetMnemonic);
const client = new RestClient(defaultHttpUrl);

View File

@ -34,16 +34,13 @@
],
"dependencies": {
"@cosmwasm/sdk": "^0.0.3",
"@iov/bcp": "^2.0.0-alpha.7",
"@iov/crypto": "^2.0.0-alpha.7",
"@iov/encoding": "^2.0.0-alpha.7",
"@iov/keycontrol": "^2.0.0-alpha.7",
"@iov/utils": "^2.0.0-alpha.7",
"argparse": "^1.0.10",
"babylon": "^6.18.0",
"colors": "^1.3.3",
"diff": "^4",
"leveldown": "^5.0.0",
"recast": "^0.18.0",
"ts-node": "^8",
"typescript": "~3.7"

View File

@ -38,28 +38,16 @@ export function main(originalArgs: readonly string[]): void {
}
const imports = new Map<string, readonly string[]>([
["@cosmwasm/sdk", ["encodeSecp256k1Signature", "makeSignBytes", "marshalTx", "types", "RestClient"]],
[
"@iov/bcp",
"@cosmwasm/sdk",
[
"Address",
"Algorithm",
"ChainId",
"Nonce",
"PrehashType",
"PubkeyBytes",
"SendTransaction",
"SignableBytes",
"TokenTicker",
"TransactionId",
// block info
"BlockInfoPending",
"BlockInfoSucceeded",
"BlockInfoFailed",
"BlockInfo",
"isBlockInfoPending",
"isBlockInfoSucceeded",
"isBlockInfoFailed",
"encodeSecp256k1Signature",
"makeSignBytes",
"marshalTx",
"Pen",
"RestClient",
"Secp256k1Pen",
"types",
],
],
[
@ -90,20 +78,6 @@ export function main(originalArgs: readonly string[]): void {
"Uint64",
],
],
[
"@iov/keycontrol",
[
"Ed25519HdWallet",
"HdPaths",
"Keyring",
"Secp256k1HdWallet",
"UserProfile",
"Wallet",
"WalletId",
"WalletImplementationIdString",
"WalletSerializationString",
],
],
["@iov/utils", ["sleep"]],
]);
@ -111,8 +85,6 @@ export function main(originalArgs: readonly string[]): void {
console.info(colors.yellow("Available imports:"));
console.info(colors.yellow(" * http"));
console.info(colors.yellow(" * https"));
console.info(colors.yellow(" * leveldown"));
console.info(colors.yellow(" * levelup"));
console.info(colors.yellow(" * from long"));
console.info(colors.yellow(" - Long"));
for (const moduleName of imports.keys()) {
@ -128,8 +100,6 @@ export function main(originalArgs: readonly string[]): void {
console.info(colors.yellow(" - toHex"));
let init = `
import leveldown = require('leveldown');
import levelup from "levelup";
import * as http from 'http';
import * as https from 'https';
import Long from "long";
@ -151,11 +121,11 @@ export function main(originalArgs: readonly string[]): void {
const hexHash = toHex(hash);
export class NewDummyClass {};
const profile = new UserProfile();
const wallet = profile.addWallet(Ed25519HdWallet.fromMnemonic("degree tackle suggest window test behind mesh extra cover prepare oak script"));
const db = levelup(leveldown('./selftest_userprofile_db'));
await profile.storeIn(db, "secret passwd");
const profileFromDb = await UserProfile.loadFrom(db, "secret passwd");
const pen = await Secp256k1Pen.fromMnemonic(
"zebra slush diet army arrest purpose hawk source west glimpse custom record",
);
const data = Encoding.toAscii("foo bar");
const signature = await pen.createSignature(data);
console.info("Done testing, will exit now.");
process.exit(0);

View File

@ -43,7 +43,5 @@
"axios": "^0.19.0"
},
"devDependencies": {
"@iov/bcp": "^2.0.0-alpha.7",
"@iov/keycontrol": "^2.0.0-alpha.7"
}
}

View File

@ -4,4 +4,5 @@ export { CosmosBech32Prefix, decodeBech32Pubkey, encodeAddress, isValidAddress }
export { unmarshalTx } from "./decoding";
export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding";
export { RestClient, TxsResponse } from "./restclient";
export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen";
export { types };

View File

@ -0,0 +1,46 @@
import { Secp256k1, Secp256k1Signature, Sha256 } from "@iov/crypto";
import { Encoding } from "@iov/encoding";
import { Secp256k1Pen } from "./pen";
const { fromHex } = Encoding;
describe("Sec256k1Pen", () => {
it("can be constructed", async () => {
const pen = await Secp256k1Pen.fromMnemonic(
"zebra slush diet army arrest purpose hawk source west glimpse custom record",
);
expect(pen).toBeTruthy();
});
describe("pubkey", () => {
it("returns compressed pubkey", async () => {
// special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling
// m/44'/118'/0'/0/0
// pubkey: 02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6
const pen = await Secp256k1Pen.fromMnemonic(
"special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling",
);
expect(pen.pubkey).toEqual(
fromHex("02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6"),
);
});
});
describe("createSignature", () => {
it("creates correct signatures", async () => {
const pen = await Secp256k1Pen.fromMnemonic(
"special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling",
);
const data = Encoding.toAscii("foo bar");
const signature = await pen.createSignature(data);
const valid = await Secp256k1.verifySignature(
new Secp256k1Signature(signature.slice(0, 32), signature.slice(32, 64)),
new Sha256(data).digest(),
pen.pubkey,
);
expect(valid).toEqual(true);
});
});
});

86
packages/sdk/src/pen.ts Normal file
View File

@ -0,0 +1,86 @@
import {
Bip39,
EnglishMnemonic,
Secp256k1,
Sha256,
Sha512,
Slip10,
Slip10Curve,
Slip10RawIndex,
} from "@iov/crypto";
export type PrehashType = "sha256" | "sha512" | null;
/**
* A pen is the most basic tool you can think of for signing. It works
* everywhere and can be used intuitively by everyone. However, it does not
* come with a great amount of features. End of semi suitable metaphor.
*
* This wraps a single keypair and allows for signing.
*
* Non-goals of this types are: multi account support, persistency, data migrations,
* obfuscation of sensitive data.
*/
export interface Pen {
readonly pubkey: Uint8Array;
readonly createSignature: (signBytes: Uint8Array, prehashType?: PrehashType) => Promise<Uint8Array>;
}
function prehash(bytes: Uint8Array, type: PrehashType): Uint8Array {
switch (type) {
case null:
return new Uint8Array([...bytes]);
case "sha256":
return new Sha256(bytes).digest();
case "sha512":
return new Sha512(bytes).digest();
default:
throw new Error("Unknown prehash type");
}
}
/**
* The Cosmoshub derivation path in the form `m/44'/118'/0'/0/a`
* with 0-based account index `a`.
*/
export function makeCosmoshubPath(a: number): readonly Slip10RawIndex[] {
return [
Slip10RawIndex.hardened(44),
Slip10RawIndex.hardened(118),
Slip10RawIndex.hardened(0),
Slip10RawIndex.normal(0),
Slip10RawIndex.normal(a),
];
}
export class Secp256k1Pen implements Pen {
public static async fromMnemonic(
mnemonic: string,
hdPath: readonly Slip10RawIndex[] = makeCosmoshubPath(0),
): Promise<Secp256k1Pen> {
const seed = await Bip39.mnemonicToSeed(new EnglishMnemonic(mnemonic));
const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, hdPath);
const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey;
return new Secp256k1Pen(privkey, Secp256k1.compressPubkey(uncompressed));
}
public readonly pubkey: Uint8Array;
private readonly privkey: Uint8Array;
private constructor(privkey: Uint8Array, pubkey: Uint8Array) {
this.privkey = privkey;
this.pubkey = pubkey;
}
/**
* Creates a fixed length encoding of the signature parameters r (32 bytes) and s (32 bytes).
*/
public async createSignature(
signBytes: Uint8Array,
prehashType: PrehashType = "sha256",
): Promise<Uint8Array> {
const message = prehash(signBytes, prehashType);
const signature = await Secp256k1.createSignature(message, this.privkey);
return new Uint8Array([...signature.r(32), ...signature.s(32)]);
}
}

View File

@ -1,12 +1,11 @@
/* eslint-disable @typescript-eslint/camelcase */
import { ChainId, Identity, PrehashType, SignableBytes } from "@iov/bcp";
import { Random } from "@iov/crypto";
import { Bech32, Encoding } from "@iov/encoding";
import { HdPaths, Secp256k1HdWallet } from "@iov/keycontrol";
import { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding";
import { leb128Encode } from "./leb128.spec";
import { Attribute, Log, parseLogs } from "./logs";
import { Pen, Secp256k1Pen } from "./pen";
import { PostTxsResponse, RestClient } from "./restclient";
import contract from "./testdata/contract.json";
import cosmoshub from "./testdata/cosmoshub.json";
@ -28,7 +27,6 @@ const httpUrl = "http://localhost:1317";
const defaultNetworkId = "testing";
const faucetMnemonic =
"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone";
const faucetPath = HdPaths.cosmos(0);
const faucetAddress = "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6";
const emptyAddress = "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k";
@ -96,11 +94,7 @@ function findAttribute(logs: readonly Log[], eventType: "message" | "transfer",
return out;
}
async function uploadContract(
client: RestClient,
wallet: Secp256k1HdWallet,
signer: Identity,
): Promise<PostTxsResponse> {
async function uploadContract(client: RestClient, pen: Pen): Promise<PostTxsResponse> {
const memo = "My first contract on chain";
const theMsg: MsgStoreCode = {
type: "wasm/store-code",
@ -122,17 +116,15 @@ async function uploadContract(
};
const account = (await client.authAccounts(faucetAddress)).result.value;
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes;
const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256);
const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature);
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account);
const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
return client.postTx(marshalTx(signedTx));
}
async function instantiateContract(
client: RestClient,
wallet: Secp256k1HdWallet,
signer: Identity,
pen: Pen,
codeId: number,
beneficiaryAddress: string,
transferAmount: readonly Coin[],
@ -161,17 +153,15 @@ async function instantiateContract(
};
const account = (await client.authAccounts(faucetAddress)).result.value;
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes;
const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256);
const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature);
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account);
const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
return client.postTx(marshalTx(signedTx));
}
async function executeContract(
client: RestClient,
wallet: Secp256k1HdWallet,
signer: Identity,
pen: Pen,
contractAddress: string,
): Promise<PostTxsResponse> {
const memo = "Time for action";
@ -195,9 +185,8 @@ async function executeContract(
};
const account = (await client.authAccounts(faucetAddress)).result.value;
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes;
const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256);
const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature);
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account);
const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
return client.postTx(marshalTx(signedTx));
}
@ -240,8 +229,7 @@ describe("RestClient", () => {
describe("post", () => {
it("can send tokens", async () => {
pendingWithoutCosmos();
const wallet = Secp256k1HdWallet.fromMnemonic(faucetMnemonic);
const signer = await wallet.createIdentity("abc" as ChainId, faucetPath);
const pen = await Secp256k1Pen.fromMnemonic(faucetMnemonic);
const memo = "My first contract on chain";
const theMsg: MsgSend = {
@ -271,9 +259,8 @@ describe("RestClient", () => {
const client = new RestClient(httpUrl);
const account = (await client.authAccounts(faucetAddress)).result.value;
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes;
const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256);
const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature);
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account);
const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
const result = await client.postTx(marshalTx(signedTx));
// console.log("Raw log:", result.raw_log);
@ -282,8 +269,7 @@ describe("RestClient", () => {
it("can upload, instantiate and execute wasm", async () => {
pendingWithoutCosmos();
const wallet = Secp256k1HdWallet.fromMnemonic(faucetMnemonic);
const signer = await wallet.createIdentity("abc" as ChainId, faucetPath);
const pen = await Secp256k1Pen.fromMnemonic(faucetMnemonic);
const client = new RestClient(httpUrl);
const transferAmount: readonly Coin[] = [
@ -303,7 +289,7 @@ describe("RestClient", () => {
// upload
{
// console.log("Raw log:", result.raw_log);
const result = await uploadContract(client, wallet, signer);
const result = await uploadContract(client, pen);
expect(result.code).toBeFalsy();
const logs = parseSuccess(result.raw_log);
const codeIdAttr = findAttribute(logs, "message", "code_id");
@ -316,14 +302,7 @@ describe("RestClient", () => {
// instantiate
{
const result = await instantiateContract(
client,
wallet,
signer,
codeId,
beneficiaryAddress,
transferAmount,
);
const result = await instantiateContract(client, pen, codeId, beneficiaryAddress, transferAmount);
expect(result.code).toBeFalsy();
// console.log("Raw log:", result.raw_log);
const logs = parseSuccess(result.raw_log);
@ -338,7 +317,7 @@ describe("RestClient", () => {
// execute
{
const result = await executeContract(client, wallet, signer, contractAddress);
const result = await executeContract(client, pen, contractAddress);
expect(result.code).toBeFalsy();
// console.log("Raw log:", result.raw_log);
const [firstLog] = parseSuccess(result.raw_log);
@ -356,8 +335,7 @@ describe("RestClient", () => {
describe("query", () => {
it("can list upload code", async () => {
pendingWithoutCosmos();
const wallet = Secp256k1HdWallet.fromMnemonic(faucetMnemonic);
const signer = await wallet.createIdentity("abc" as ChainId, faucetPath);
const pen = await Secp256k1Pen.fromMnemonic(faucetMnemonic);
const client = new RestClient(httpUrl);
// check with contracts were here first to compare
@ -366,7 +344,7 @@ describe("RestClient", () => {
const numExisting = existingInfos.length;
// upload data
const result = await uploadContract(client, wallet, signer);
const result = await uploadContract(client, pen);
expect(result.code).toBeFalsy();
const logs = parseSuccess(result.raw_log);
const codeIdAttr = findAttribute(logs, "message", "code_id");
@ -387,8 +365,7 @@ describe("RestClient", () => {
it("can list contracts and get info", async () => {
pendingWithoutCosmos();
const wallet = Secp256k1HdWallet.fromMnemonic(faucetMnemonic);
const signer = await wallet.createIdentity("abc" as ChainId, faucetPath);
const pen = await Secp256k1Pen.fromMnemonic(faucetMnemonic);
const client = new RestClient(httpUrl);
const beneficiaryAddress = makeRandomAddress();
const transferAmount: readonly Coin[] = [
@ -404,7 +381,7 @@ describe("RestClient", () => {
if (existingInfos.length > 0) {
codeId = existingInfos[existingInfos.length - 1].id;
} else {
const uploaded = await uploadContract(client, wallet, signer);
const uploaded = await uploadContract(client, pen);
expect(uploaded.code).toBeFalsy();
const uploadLogs = parseSuccess(uploaded.raw_log);
const codeIdAttr = findAttribute(uploadLogs, "message", "code_id");
@ -414,14 +391,7 @@ describe("RestClient", () => {
// create new instance and compare before and after
const existingContracts = await client.listContractAddresses();
const result = await instantiateContract(
client,
wallet,
signer,
codeId,
beneficiaryAddress,
transferAmount,
);
const result = await instantiateContract(client, pen, codeId, beneficiaryAddress, transferAmount);
expect(result.code).toBeFalsy();
const logs = parseSuccess(result.raw_log);
const contractAddressAttr = findAttribute(logs, "message", "contract_address");

View File

@ -3,4 +3,5 @@ export { CosmosBech32Prefix, decodeBech32Pubkey, encodeAddress, isValidAddress }
export { unmarshalTx } from "./decoding";
export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding";
export { RestClient, TxsResponse } from "./restclient";
export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen";
export { types };

31
packages/sdk/types/pen.d.ts vendored Normal file
View File

@ -0,0 +1,31 @@
import { Slip10RawIndex } from "@iov/crypto";
export declare type PrehashType = "sha256" | "sha512" | null;
/**
* A pen is the most basic tool you can think of for signing. It works
* everywhere and can be used intuitively by everyone. However, it does not
* come with a great amount of features. End of semi suitable metaphor.
*
* This wraps a single keypair and allows for signing.
*
* Non-goals of this types are: multi account support, persistency, data migrations,
* obfuscation of sensitive data.
*/
export interface Pen {
readonly pubkey: Uint8Array;
readonly createSignature: (signBytes: Uint8Array, prehashType?: PrehashType) => Promise<Uint8Array>;
}
/**
* The Cosmoshub derivation path in the form `m/44'/118'/0'/0/a`
* with 0-based account index `a`.
*/
export declare function makeCosmoshubPath(a: number): readonly Slip10RawIndex[];
export declare class Secp256k1Pen implements Pen {
static fromMnemonic(mnemonic: string, hdPath?: readonly Slip10RawIndex[]): Promise<Secp256k1Pen>;
readonly pubkey: Uint8Array;
private readonly privkey;
private constructor();
/**
* Creates a fixed length encoding of the signature parameters r (32 bytes) and s (32 bytes).
*/
createSignature(signBytes: Uint8Array, prehashType?: PrehashType): Promise<Uint8Array>;
}

View File

@ -4,6 +4,7 @@
"jsRules": {},
"rules": {
"array-type": [true, "array"],
"await-promise": true,
"callable-types": false,
"comment-format": [true, "check-space"],
"curly": false,

View File

@ -4898,15 +4898,6 @@ level-supports@~1.0.0:
dependencies:
xtend "^4.0.2"
leveldown@^5.0.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/leveldown/-/leveldown-5.4.1.tgz#83a8fdd9bb52b1ed69be2ef59822b6cdfcdb51ec"
integrity sha512-3lMPc7eU3yj5g+qF1qlALInzIYnkySIosR1AsUKFjL9D8fYbTLuENBAeDRZXIG4qeWOAyqRItOoLu2v2avWiMA==
dependencies:
abstract-leveldown "~6.2.1"
napi-macros "~2.0.0"
node-gyp-build "~4.1.0"
levelup@^4.0.0:
version "4.3.2"
resolved "https://registry.yarnpkg.com/levelup/-/levelup-4.3.2.tgz#31c5b1b29f146d1d35d692e01a6da4d28fa55ebd"
@ -5517,11 +5508,6 @@ nanomatch@^1.2.9:
snapdragon "^0.8.1"
to-regex "^3.0.1"
napi-macros@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b"
integrity sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@ -5556,11 +5542,6 @@ node-fetch@^2.3.0, node-fetch@^2.5.0:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
node-gyp-build@~4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz#d7270b5d86717068d114cc57fff352f96d745feb"
integrity sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==
node-gyp@^5.0.2:
version "5.0.7"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-5.0.7.tgz#dd4225e735e840cf2870e4037c2ed9c28a31719e"