New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@fivebinaries/coin-selection

Package Overview
Dependencies
Maintainers
3
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@fivebinaries/coin-selection - npm Package Compare versions

Comparing version 0.0.1-beta.2 to 0.0.1-beta.3

CHANGELOG.MD

15

lib/constants/index.d.ts

@@ -18,2 +18,15 @@ export declare const dummyAddress = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp";

};
export declare const MIN_UTXO_VALUE = "1000000";
export declare const CARDANO_PARAMS: {
readonly PROTOCOL_MAGICS: {
readonly mainnet: number;
readonly testnet: number;
};
readonly NETWORK_IDS: {
readonly mainnet: number;
readonly testnet: number;
};
readonly MIN_UTXO_VALUE: "1000000";
readonly MAX_TX_SIZE: 16384;
readonly MAX_VALUE_SIZE: 5000;
};
export declare const MAX_TOKENS_PER_OUTPUT = 50;

@@ -0,1 +1,2 @@

import { NetworkInfo } from '@emurgo/cardano-serialization-lib-nodejs';
export const dummyAddress = 'addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp';

@@ -18,2 +19,18 @@ export const CertificateType = {

};
export const MIN_UTXO_VALUE = '1000000';
export const CARDANO_PARAMS = {
PROTOCOL_MAGICS: {
mainnet: NetworkInfo.mainnet().protocol_magic(),
testnet: NetworkInfo.testnet().protocol_magic(),
},
NETWORK_IDS: {
mainnet: NetworkInfo.mainnet().network_id(),
testnet: NetworkInfo.testnet().network_id(),
},
MIN_UTXO_VALUE: '1000000',
MAX_TX_SIZE: 16384,
MAX_VALUE_SIZE: 5000,
};
// https://github.com/vacuumlabs/adalite/blob/d8ba3bb1ff439ae8e02abd99163435a989d97961/app/frontend/wallet/shelley/transaction/constants.ts
// policyId is 28 bytes, assetName max 32 bytes, together with quantity makes
// max token size about 70 bytes, max output size is 4000 => 4000 / 70 ~ 50
export const MAX_TOKENS_PER_OUTPUT = 50;

126

lib/methods/largestFirst.js
import { ERROR } from '../constants';
import * as CardanoWasm from '@emurgo/cardano-serialization-lib-browser';
import { bigNumFromStr, calculateRequiredDeposit, getAssetAmount, getInputCost, getOutputCost, prepareCertificates, prepareChangeOutput, prepareWithdrawals, setMinUtxoValueForOutputs, sortUtxos, getTxBuilder, assetsAmountSatisfied, getInitialUtxoSet, setMaxOutput, getTotalUserOutputsAmount, multiAssetToArray, buildTxInput, buildTxOutput, } from '../utils/common';
import { bigNumFromStr, calculateRequiredDeposit, getAssetAmount, getOutputCost, prepareCertificates, prepareChangeOutput, prepareWithdrawals, setMinUtxoValueForOutputs, sortUtxos, getTxBuilder, getInitialUtxoSet, setMaxOutput, getTotalUserOutputsAmount, multiAssetToArray, buildTxInput, buildTxOutput, getUnsatisfiedAssets, splitChangeOutput, } from '../utils/common';
import { CoinSelectionError } from '../utils/errors';

@@ -8,3 +8,3 @@ export const largestFirst = (utxos, outputs, changeAddress, certificates, withdrawals, accountPubKey, options) => {

const txBuilder = getTxBuilder((_a = options === null || options === void 0 ? void 0 : options.feeParams) === null || _a === void 0 ? void 0 : _a.a);
let usedUtxos = [];
const usedUtxos = [];
let sortedUtxos = sortUtxos(utxos);

@@ -32,13 +32,16 @@ const accountKey = CardanoWasm.Bip32PublicKey.from_bytes(Buffer.from(accountPubKey, 'hex'));

const preparedOutputs = setMinUtxoValueForOutputs(txBuilder, outputs);
const addUtxoToSelection = (utxo) => {
const { input, address, amount } = buildTxInput(utxo);
const fee = txBuilder.fee_for_input(address, input, amount);
txBuilder.add_input(address, input, amount);
usedUtxos.push(utxo);
totalFeesAmount = totalFeesAmount.checked_add(fee);
utxosTotalAmount = utxosTotalAmount.checked_add(bigNumFromStr(getAssetAmount(utxo)));
};
// set initial utxos set for setMax functionality
const maxOutput = preparedOutputs[outputs.findIndex(o => !!o.setMax)];
const maxOutputIndex = outputs.findIndex(o => !!o.setMax);
const maxOutput = preparedOutputs[maxOutputIndex];
const { used, remaining } = getInitialUtxoSet(sortedUtxos, maxOutput);
usedUtxos = used;
sortedUtxos = remaining;
usedUtxos.forEach(utxo => {
// this should really be a function
const { inputFee } = getInputCost(txBuilder, utxo);
totalFeesAmount = totalFeesAmount.checked_add(inputFee);
utxosTotalAmount = utxosTotalAmount.checked_add(bigNumFromStr(getAssetAmount(utxo)));
});
used.forEach(utxo => addUtxoToSelection(utxo));
// Calculate fee and minUtxoValue for all external outputs

@@ -49,56 +52,54 @@ const outputsCost = preparedOutputs.map(output => getOutputCost(txBuilder, output));

totalFeesAmount = totalFeesAmount.checked_add(totalOutputsFee);
let totalUserOutputsAmount = getTotalUserOutputsAmount(preparedOutputs, deposit);
let changeOutput = null;
let sufficientUtxos = false;
let totalUserOutputsAmount = getTotalUserOutputsAmount(preparedOutputs, deposit);
let forceAnotherRound = false;
while (!sufficientUtxos) {
if (maxOutput) {
// reset previously computed maxOutput in order to correctly calculate a potential change output
preparedOutputs[maxOutputIndex] = setMinUtxoValueForOutputs(txBuilder, [
maxOutput,
])[0];
}
// Calculate change output
let preparedChangeOutput = prepareChangeOutput(txBuilder, usedUtxos, preparedOutputs, changeAddress, utxosTotalAmount, getTotalUserOutputsAmount(preparedOutputs, deposit), totalFeesAmount);
let singleChangeOutput = prepareChangeOutput(txBuilder, usedUtxos, preparedOutputs, changeAddress, utxosTotalAmount, getTotalUserOutputsAmount(preparedOutputs, deposit), totalFeesAmount);
if (maxOutput) {
// set amount for the max output
const { changeOutput: newChangeOutput } = setMaxOutput(maxOutput, preparedChangeOutput);
// set amount for a max output from a changeOutput calculated above
const { changeOutput: newChangeOutput, maxOutput: newMaxOutput } = setMaxOutput(maxOutput, singleChangeOutput);
// change output may be completely removed if all ADA are consumed by max output
preparedChangeOutput = newChangeOutput;
singleChangeOutput = newChangeOutput;
preparedOutputs[maxOutputIndex] = newMaxOutput;
// recalculate total user outputs amount
totalUserOutputsAmount = getTotalUserOutputsAmount(preparedOutputs, deposit);
}
const changeOutputs = singleChangeOutput
? splitChangeOutput(txBuilder, singleChangeOutput, changeAddress, options === null || options === void 0 ? void 0 : options._maxTokensPerOutput)
: [];
let requiredAmount = totalFeesAmount.checked_add(totalUserOutputsAmount);
if (preparedChangeOutput) {
changeOutputs.forEach(changeOutput => {
// we need to cover amounts and fees for change outputs
requiredAmount = requiredAmount
.checked_add(preparedChangeOutput.output.amount().coin())
.checked_add(preparedChangeOutput.outputFee);
}
// console.log('----CURRENT UTXO SELECTION ITERATION----');
// console.log('usedUtxos', usedUtxos);
// console.log(
// 'utxosTotalAmount (ADA amount that needs to be spent (sum of utxos = outputs - fee))',
// utxosTotalAmount.to_str(),
// );
// console.log(
// 'requiredAmount to cover fees for all inputs, outputs (including additional change output if needed) and output amounts themselves',
// requiredAmount.to_str(),
// );
// console.log(
// `CHANGE OUTPUT (already included in requiredAmount above): amount: ${
// preparedChangeOutput?.output.amount
// } fe: ${preparedChangeOutput?.outputFee.to_str()}`,
// );
// console.log('addAnotherUtxo', addAnotherUtxo);
.checked_add(changeOutput.output.amount().coin())
.checked_add(changeOutput.outputFee);
});
// List of tokens for which we don't have enough utxos
const unsatisfiedAssets = getUnsatisfiedAssets(usedUtxos, preparedOutputs);
if (utxosTotalAmount.compare(requiredAmount) >= 0 &&
assetsAmountSatisfied(usedUtxos, preparedOutputs) &&
unsatisfiedAssets.length === 0 &&
usedUtxos.length > 0 && // TODO: force at least 1 utxo, otherwise withdrawal tx is composed without utxo if rewards > tx cost
!forceAnotherRound) {
// we are done. we have enough utxos to cover fees + minUtxoValue for each output. now we can add the cost of the change output to total fees
if (preparedChangeOutput) {
totalFeesAmount = totalFeesAmount.checked_add(preparedChangeOutput.outputFee);
if (changeOutputs.length > 0) {
changeOutputs.forEach(changeOutput => {
totalFeesAmount = totalFeesAmount.checked_add(changeOutput.outputFee);
});
// set change output
changeOutput = {
changeOutput = changeOutputs.map(change => ({
isChange: true,
amount: preparedChangeOutput.output.amount().coin().to_str(),
amount: change.output.amount().coin().to_str(),
address: changeAddress,
assets: multiAssetToArray(preparedChangeOutput.output.amount().multiasset()),
};
assets: multiAssetToArray(change.output.amount().multiasset()),
}));
}
else {
const unspendableChangeAmount = utxosTotalAmount.clamped_sub(totalFeesAmount.checked_add(totalUserOutputsAmount));
if (sortedUtxos.length > 0) {

@@ -110,6 +111,4 @@ // In current iteration we don't have enough utxo to meet min utxo value for an output,

}
// console.warn(
// `Change output would be inefficient. Burning ${UnspendableChangeAmount.to_str()} as fee`,
// );
// Since we didn't add a change output we can burn its value + fee we would pay for it. That's equal to initial placeholderChangeOutputAmount
// Change output would be inefficient., we can burn its value + fee we would pay for it
const unspendableChangeAmount = utxosTotalAmount.clamped_sub(totalFeesAmount.checked_add(totalUserOutputsAmount));
totalFeesAmount = totalFeesAmount.checked_add(unspendableChangeAmount);

@@ -120,13 +119,12 @@ }

else {
if (unsatisfiedAssets.length > 0) {
sortedUtxos = sortUtxos(sortedUtxos, unsatisfiedAssets[0]);
}
else {
sortedUtxos = sortUtxos(sortedUtxos);
}
const utxo = sortedUtxos.shift();
if (!utxo)
break;
usedUtxos.push(utxo);
const { input, address, amount } = buildTxInput(utxo);
const inputFee = txBuilder.fee_for_input(address, input, amount);
txBuilder.add_input(address, input, amount);
// const { inputFee } = getInputCost(txBuilder, utxo);
// add input fee to total
totalFeesAmount = totalFeesAmount.checked_add(inputFee);
utxosTotalAmount = utxosTotalAmount.checked_add(bigNumFromStr(getAssetAmount(utxo)));
addUtxoToSelection(utxo);
forceAnotherRound = false;

@@ -145,4 +143,6 @@ }

if (changeOutput) {
finalOutputs.push(changeOutput);
txBuilder.add_output(buildTxOutput(changeOutput));
changeOutput.forEach(change => {
finalOutputs.push(change);
txBuilder.add_output(buildTxOutput(change));
});
}

@@ -154,11 +154,3 @@ txBuilder.set_fee(totalFeesAmount);

const totalSpent = totalUserOutputsAmount.checked_add(totalFeesAmount);
// console.log('FINAL RESULT START==========');
// console.log('inputs', usedUtxos);
// console.log('outputs', finalOutputs);
// console.log('totalOutputAmount', totalOutputAmount.to_str());
// console.log('utxosTotalAmount', utxosTotalAmount.to_str());
// console.log('totalSpent', totalSpent.to_str());
// console.log('deposit', deposit.to_str());
// console.log('withdrawal', totalWithdrawal.to_str());
// console.log('FINAL RESULT END');
// Set max property with the value of an output which has setMax=true
let max;

@@ -178,5 +170,5 @@ if (maxOutput) {

withdrawal: totalWithdrawal.to_str(),
tx: { body: txBodyHex, hash: txHash },
tx: { body: txBodyHex, hash: txHash, size: txBuilder.full_size() },
max,
};
};

@@ -64,2 +64,3 @@ import * as CardanoWasm from '@emurgo/cardano-serialization-lib-browser';

hash: string;
size: number;
};

@@ -104,2 +105,3 @@ inputs: Utxo[];

};
_maxTokensPerOutput?: number;
}
import * as CardanoWasm from '@emurgo/cardano-serialization-lib-browser';
import { CARDANO_PARAMS } from '../constants';
import { Certificate, Output, Utxo, Withdrawal, OutputCost, UserOutput, Asset } from '../types/types';
export declare const CARDANO: {
readonly PROTOCOL_MAGICS: {
readonly mainnet: 764824073;
readonly testnet: 1097911063;
};
readonly NETWORK_IDS: {
readonly mainnet: 1;
readonly testnet: 0;
};
readonly ADDRESS_TYPE: {
readonly Base: 0;
readonly Pointer: 4;
readonly Enterprise: 6;
readonly Byron: 8;
readonly Reward: 14;
};
readonly CERTIFICATE_TYPE: {
readonly StakeRegistration: 0;
readonly StakeDeregistration: 1;
readonly StakeDelegation: 2;
readonly StakePoolRegistration: 3;
};
};
export declare const bigNumFromStr: (num: string) => CardanoWasm.BigNum;
export declare const getProtocolMagic: (tesnet?: boolean | undefined) => typeof CARDANO.PROTOCOL_MAGICS['mainnet'] | typeof CARDANO.PROTOCOL_MAGICS['testnet'];
export declare const getNetworkId: (testnet?: boolean | undefined) => typeof CARDANO.NETWORK_IDS['mainnet'] | typeof CARDANO.NETWORK_IDS['testnet'];
export declare const getProtocolMagic: (tesnet?: boolean | undefined) => typeof CARDANO_PARAMS.PROTOCOL_MAGICS['mainnet'] | typeof CARDANO_PARAMS.PROTOCOL_MAGICS['testnet'];
export declare const getNetworkId: (testnet?: boolean | undefined) => typeof CARDANO_PARAMS.NETWORK_IDS['mainnet'] | typeof CARDANO_PARAMS.NETWORK_IDS['testnet'];
export declare const parseAsset: (hex: string) => {

@@ -45,6 +23,2 @@ policyId: string;

};
export declare const getInputCost: (txBuilder: CardanoWasm.TransactionBuilder, utxo: Utxo) => {
input: CardanoWasm.TransactionInput;
inputFee: CardanoWasm.BigNum;
};
export declare const buildTxOutput: (output: Output) => CardanoWasm.TransactionOutput;

@@ -56,5 +30,6 @@ export declare const getOutputCost: (txBuilder: CardanoWasm.TransactionBuilder, output: Output) => OutputCost;

export declare const setMinUtxoValueForOutputs: (txBuilder: CardanoWasm.TransactionBuilder, outputs: UserOutput[]) => UserOutput[];
export declare const splitChangeOutput: (txBuilder: CardanoWasm.TransactionBuilder, singleChangeOutput: OutputCost, changeAddress: string, maxTokensPerOutput?: number) => OutputCost[];
export declare const prepareChangeOutput: (txBuilder: CardanoWasm.TransactionBuilder, usedUtxos: Utxo[], preparedOutputs: Output[], changeAddress: string, utxosTotalAmount: CardanoWasm.BigNum, totalOutputAmount: CardanoWasm.BigNum, totalFeesAmount: CardanoWasm.BigNum) => OutputCost | null;
export declare const getTxBuilder: (a?: string) => CardanoWasm.TransactionBuilder;
export declare const assetsAmountSatisfied: (utxos: Utxo[], outputs: Output[]) => boolean;
export declare const getUnsatisfiedAssets: (utxos: Utxo[], outputs: Output[]) => string[];
export declare const getInitialUtxoSet: (utxos: Utxo[], maxOutput: UserOutput | undefined) => {

@@ -61,0 +36,0 @@ used: Utxo[];

import * as CardanoWasm from '@emurgo/cardano-serialization-lib-browser';
import { CertificateType, dummyAddress, ERROR, MIN_UTXO_VALUE, } from '../constants';
import { CARDANO_PARAMS, CertificateType, dummyAddress, ERROR, MAX_TOKENS_PER_OUTPUT, } from '../constants';
import { CoinSelectionError } from './errors';
export const CARDANO = {
PROTOCOL_MAGICS: {
mainnet: 764824073,
testnet: 1097911063,
},
NETWORK_IDS: {
mainnet: 1,
testnet: 0,
},
ADDRESS_TYPE: {
Base: 0,
Pointer: 4,
Enterprise: 6,
Byron: 8,
Reward: 14,
},
CERTIFICATE_TYPE: {
StakeRegistration: 0,
StakeDeregistration: 1,
StakeDelegation: 2,
StakePoolRegistration: 3,
},
};
export const bigNumFromStr = (num) => CardanoWasm.BigNum.from_str(num);
export const getProtocolMagic = (tesnet) => tesnet ? CARDANO.PROTOCOL_MAGICS.testnet : CARDANO.PROTOCOL_MAGICS.mainnet;
export const getNetworkId = (testnet) => testnet ? CARDANO.NETWORK_IDS.testnet : CARDANO.NETWORK_IDS.mainnet;
export const getProtocolMagic = (tesnet) => tesnet
? CARDANO_PARAMS.PROTOCOL_MAGICS.testnet
: CARDANO_PARAMS.PROTOCOL_MAGICS.mainnet;
export const getNetworkId = (testnet) => testnet
? CARDANO_PARAMS.NETWORK_IDS.testnet
: CARDANO_PARAMS.NETWORK_IDS.mainnet;
export const parseAsset = (hex) => {

@@ -77,3 +58,3 @@ const policyIdSize = 56;

export const getMinAdaRequired = (multiAsset) => {
const minUtxoValue = bigNumFromStr(MIN_UTXO_VALUE);
const minUtxoValue = bigNumFromStr(CARDANO_PARAMS.MIN_UTXO_VALUE);
if (!multiAsset)

@@ -106,14 +87,5 @@ return minUtxoValue;

};
export const getInputCost = (txBuilder, utxo) => {
// Calculate additional fee required to add utxo to a transaction
const { input, address, amount } = buildTxInput(utxo);
const inputFee = txBuilder.fee_for_input(address, input, amount); // does utxoAddr make sense here?
return {
input: input,
inputFee,
};
};
export const buildTxOutput = (output) => {
var _a;
const minUtxoValue = bigNumFromStr(MIN_UTXO_VALUE);
const minUtxoValue = bigNumFromStr(CARDANO_PARAMS.MIN_UTXO_VALUE);
// this will set required minUtxoValue even if output.amount === 0

@@ -137,3 +109,3 @@ const outputAmount = output.amount && minUtxoValue.compare(bigNumFromStr(output.amount)) < 0

export const getOutputCost = (txBuilder, output) => {
const minUtxoValue = bigNumFromStr(MIN_UTXO_VALUE);
const minUtxoValue = bigNumFromStr(CARDANO_PARAMS.MIN_UTXO_VALUE);
const txOutput = buildTxOutput(output);

@@ -215,2 +187,42 @@ const outputFee = txBuilder.fee_for_output(txOutput);

};
export const splitChangeOutput = (txBuilder, singleChangeOutput, changeAddress, maxTokensPerOutput = MAX_TOKENS_PER_OUTPUT) => {
if (!singleChangeOutput.output.amount().multiasset()) {
return [singleChangeOutput];
}
let lovelaceAvailable = singleChangeOutput.output
.amount()
.coin()
.checked_add(txBuilder.fee_for_output(singleChangeOutput.output));
const allAssets = multiAssetToArray(singleChangeOutput.output.amount().multiasset());
const nAssetBundles = Math.ceil(allAssets.length / maxTokensPerOutput);
const changeOutputs = [];
// split change output to multiple outputs, where each bundle has maximum of maxTokensPerOutput assets
for (let i = 0; i < nAssetBundles; i++) {
const assetsBundle = allAssets.slice(i * maxTokensPerOutput, (i + 1) * maxTokensPerOutput);
const minAdaRequired = getMinAdaRequired(buildMultiAsset(assetsBundle));
changeOutputs.push({
isChange: true,
address: changeAddress,
amount: minAdaRequired.to_str(),
assets: assetsBundle,
});
}
const changeOutputsCost = changeOutputs.map((partialChange, i) => {
let changeOutputCost = getOutputCost(txBuilder, partialChange);
lovelaceAvailable = lovelaceAvailable.clamped_sub(bigNumFromStr(partialChange.amount).checked_add(changeOutputCost.outputFee));
if (i === changeOutputs.length - 1) {
// add all unused ADA to the last change output
let changeOutputAmount = lovelaceAvailable.checked_add(bigNumFromStr(partialChange.amount));
if (changeOutputAmount.compare(changeOutputCost.minOutputAmount) < 0) {
// computed change amount would be below minUtxoValue
// set change output amount to met minimum requirements for minUtxoValue
changeOutputAmount = changeOutputCost.minOutputAmount;
}
partialChange.amount = changeOutputAmount.to_str();
changeOutputCost = getOutputCost(txBuilder, partialChange);
}
return changeOutputCost;
});
return changeOutputsCost;
};
export const prepareChangeOutput = (txBuilder, usedUtxos, preparedOutputs, changeAddress, utxosTotalAmount, totalOutputAmount, totalFeesAmount) => {

@@ -275,9 +287,9 @@ // change output amount should be lowered by the cost of the change output (fee + minUtxoVal)

};
export const getTxBuilder = (a = '44') => CardanoWasm.TransactionBuilder.new(CardanoWasm.LinearFee.new(bigNumFromStr(a), bigNumFromStr('155381')), bigNumFromStr(MIN_UTXO_VALUE),
export const getTxBuilder = (a = '44') => CardanoWasm.TransactionBuilder.new(CardanoWasm.LinearFee.new(bigNumFromStr(a), bigNumFromStr('155381')), bigNumFromStr(CARDANO_PARAMS.MIN_UTXO_VALUE),
// pool deposit
bigNumFromStr('500000000'),
// key deposit
bigNumFromStr('2000000'));
export const assetsAmountSatisfied = (utxos, outputs) => {
let assetsAmountSatisfied = true;
bigNumFromStr('2000000'), CARDANO_PARAMS.MAX_VALUE_SIZE, CARDANO_PARAMS.MAX_TX_SIZE);
export const getUnsatisfiedAssets = (utxos, outputs) => {
const assets = [];
outputs.forEach(output => {

@@ -288,7 +300,7 @@ if (output.assets.length > 0) {

if (assetAmountInUtxos.compare(bigNumFromStr(asset.quantity)) < 0) {
assetsAmountSatisfied = false;
assets.push(asset.unit);
}
}
});
return assetsAmountSatisfied;
return assets;
};

@@ -295,0 +307,0 @@ export const getInitialUtxoSet = (utxos, maxOutput) => {

{
"name": "@fivebinaries/coin-selection",
"version": "0.0.1-beta.2",
"version": "0.0.1-beta.3",
"description": "",

@@ -24,3 +24,3 @@ "keywords": [

"devDependencies": {
"@emurgo/cardano-serialization-lib-nodejs": "^8.1.0",
"@emurgo/cardano-serialization-lib-nodejs": "^9.1.0",
"@swc-node/jest": "^1.3.1",

@@ -48,4 +48,4 @@ "@types/jest": "^26.0.24",

"dependencies": {
"@emurgo/cardano-serialization-lib-browser": "^8.1.0"
"@emurgo/cardano-serialization-lib-browser": "^9.1.0"
}
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc