Comparing version 1.2.2 to 1.3.0
@@ -0,1 +1,9 @@ | ||
# 1.3.0 | ||
## Added | ||
- Support for key registration transactions | ||
- Support for flat fees | ||
- Signing and verifying bytes | ||
## Fixed | ||
- deleteMultisig uses post instead of get | ||
- "MultiSig" standardized to "Multisig" | ||
# 1.2.2 | ||
@@ -2,0 +10,0 @@ ## Added |
{ | ||
"name": "algosdk", | ||
"version": "1.2.2", | ||
"version": "1.3.0", | ||
"description": "algosdk is Algorand's official javascript SDK", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -45,3 +45,3 @@ const client = require('./client'); | ||
/** | ||
* helathCheck returns an empty object iff the node is running | ||
* healthCheck returns an empty object iff the node is running | ||
* @returns {Promise<*>} | ||
@@ -106,5 +106,6 @@ */ | ||
* @param last number, optional | ||
* @param maxTxns number, optional | ||
* @returns {Promise<*>} | ||
*/ | ||
this.transactionByAddress = async function (addr, first=null, last=null) { | ||
this.transactionByAddress = async function (addr, first=null, last=null, maxTxns=null) { | ||
@@ -117,3 +118,3 @@ if (( first !== null ) && (!Number.isInteger(first) )){ | ||
} | ||
let res = await c.get("/v1/account/" + addr + "/transactions", { 'firstRound': first, 'lastRound': last }); | ||
let res = await c.get("/v1/account/" + addr + "/transactions", { 'firstRound': first, 'lastRound': last, 'max': maxTxns }); | ||
if (res.statusCode === 200 && res.body.transactions !== undefined) { | ||
@@ -128,2 +129,21 @@ for (let i = 0; i < res.body.transactions.length; i++) { | ||
/** | ||
* transactionsByAddressAndDate returns all transactions for a PK [addr] in the [fromDate, toDate] date range. | ||
* The date is a string in the YYYY-MM-DD format. | ||
* @param addr string | ||
* @param fromDate string | ||
* @param toDate string | ||
* @param maxTxns number, optional | ||
* @returns {Promise<*>} | ||
*/ | ||
this.transactionByAddressAndDate = async function (addr, fromDate, toDate, maxTxns=null) { | ||
let res = await c.get("/v1/account/" + addr + "/transactions", { 'fromDate': fromDate, 'toDate': toDate, 'max': maxTxns }); | ||
if (res.statusCode === 200 && res.body.transactions !== undefined) { | ||
for (let i = 0; i < res.body.transactions.length; i++) { | ||
res.body.transactions[i] = noteb64ToNote(res.body.transactions[i]); | ||
} | ||
} | ||
return res.body; | ||
}; | ||
/** | ||
* transactionById returns the a transaction information of a specific txid [txId] | ||
@@ -140,3 +160,3 @@ * Note - This method is allowed only when Indexer is enabled. | ||
/** | ||
* transactionInformation returns the a transaction information of a specific txid and an address | ||
* transactionInformation returns the transaction information of a specific txid and an address | ||
* @param addr | ||
@@ -155,2 +175,15 @@ * @param txid | ||
/** | ||
* pendingTransactionInformation returns the transaction information for a specific txid of a pending transaction | ||
* @param txid | ||
* @returns {Promise<*>} | ||
*/ | ||
this.pendingTransactionInformation = async function (txid) { | ||
let res = await c.get("/v1/transactions/pending/" + txid); | ||
if (res.statusCode === 200) { | ||
res.body = noteb64ToNote(res.body); | ||
} | ||
return res.body; | ||
}; | ||
/** | ||
* accountInformation returns the passed account's information | ||
@@ -157,0 +190,0 @@ * @param addr string |
@@ -354,3 +354,20 @@ const client = require('./client'); | ||
/** | ||
* deleteMultisig accepts a wallet handle, wallet password, and multisig | ||
* address, and deletes the information about this multisig address from the | ||
* wallet (including address and secret key). | ||
* @param walletHandle | ||
* @param walletPassword | ||
* @param addr | ||
* @returns {Promise<*>} | ||
*/ | ||
this.deleteMultisig = async function (walletHandle, walletPassword, addr) { | ||
let req = { | ||
"wallet_handle_token": walletHandle, | ||
"address": addr, | ||
"wallet_password": walletPassword | ||
}; | ||
let res = await c.delete("/v1/multisig", req); | ||
return res.body; | ||
}; | ||
} | ||
@@ -357,0 +374,0 @@ module.exports = {Kmd}; |
@@ -10,2 +10,3 @@ const nacl = require('./nacl/naclWrappers'); | ||
const kmd = require('./client/kmd'); | ||
const utils = require('./utils/utils'); | ||
@@ -15,8 +16,9 @@ let Algod = algod.Algod; | ||
const SIGN_BYTES_PREFIX = Buffer.from([77, 88]); // "MX" | ||
const MICROALGOS_TO_ALGOS_RATIO = 1e6; | ||
// Errors | ||
const ERROR_MULTISIG_BAD_SENDER = new Error("The transaction sender address and multisig preimage do not match."); | ||
const ERROR_INVALID_MICROALGOS = new Error("Microalgos should be positive and less than 2^53 - 1.") | ||
/** | ||
* GenerateAddress returns a new Algorand address and its corresponding secret key | ||
* generateAccount returns a new Algorand address and its corresponding secret key | ||
* @returns {{sk: Uint8Array, addr: string}} | ||
@@ -33,3 +35,3 @@ */ | ||
* @param addr Algorand address | ||
* @returns {boolean}n true if valid, false otherwise | ||
* @returns {boolean} true if valid, false otherwise | ||
*/ | ||
@@ -84,7 +86,13 @@ function isValidAddress(addr) { | ||
/** | ||
* signTransaction takes an object with the following fields: to, amount, fee per byte, firstRound, lastRound, | ||
* and note(optional),GenesisID(optional) and a secret key and returns a signed blob | ||
* @param txn object with the following fields - to, amount, fee per byte, firstRound, lastRound, and note(optional) | ||
* signTransaction takes an object with either payment or key registration fields and | ||
* a secret key and returns a signed blob. | ||
* | ||
* Payment transaction fields: to, amount, fee, firstRound, lastRound, genesisHash, | ||
* note(optional), GenesisID(optional), closeRemainderTo(optional) | ||
* | ||
* Key registration fields: fee, firstRound, lastRound, voteKey, selectionKey, voteFirst, | ||
* voteLast, voteKeyDilution, genesisHash, note(optional), GenesisID(optional) | ||
* @param txn object with either payment or key registration fields | ||
* @param sk Algorand Secret Key | ||
* @returns object contains the binary signed transaction and it's txID | ||
* @returns object contains the binary signed transaction and its txID | ||
*/ | ||
@@ -113,6 +121,33 @@ function signTransaction(txn, sk) { | ||
/** | ||
* signBytes takes arbitrary bytes and a secret key, prepends the bytes with "MX" for domain separation, signs the bytes | ||
* with the private key, and returns the signature. | ||
* @param bytes Uint8array | ||
* @param sk Algorand secret key | ||
* @returns binary signature | ||
*/ | ||
function signBytes(bytes, sk) { | ||
let toBeSigned = Buffer.from(utils.concatArrays(SIGN_BYTES_PREFIX, bytes)); | ||
let sig = nacl.sign(toBeSigned, sk); | ||
return sig; | ||
} | ||
/** | ||
* verifyBytes takes arbitraray bytes, an address, and a signature and verifies if the signature is correct for the public | ||
* key and the bytes (the bytes should have been signed with "MX" prepended for domain separation). | ||
* @param bytes Uint8Array | ||
* @param signature binary signature | ||
* @param addr string address | ||
* @returns bool | ||
*/ | ||
function verifyBytes(bytes, signature, addr) { | ||
toBeVerified = Buffer.from(utils.concatArrays(SIGN_BYTES_PREFIX, bytes)); | ||
let pk = address.decode(addr).publicKey; | ||
return nacl.verify(toBeVerified, signature, pk); | ||
} | ||
/** | ||
* signMultisigTransaction takes a raw transaction (see signTransaction), a multisig preimage, a secret key, and returns | ||
* a multisig transaction, which is a blob representing a transaction and multisignature account preimage. The returned | ||
* multisig txn can accumulate additional signatures through mergeMultisigTransactions or appendMultisigTransaction. | ||
* @param txn object with the following fields - to, amount, fee per byte, firstRound, lastRound, and note(optional) | ||
* @param txn object with either payment or key registration fields | ||
* @param version multisig version | ||
@@ -150,3 +185,3 @@ * @param threshold multisig threshold | ||
* @param version multisig version | ||
* @param threshold mutlisig threshold | ||
* @param threshold multisig threshold | ||
* @param addrs a list of Algorand addresses representing possible signers for this multisig. Order is important. | ||
@@ -208,2 +243,25 @@ * @param sk Algorand secret key | ||
/** | ||
* microalgosToAlgos converts microalgos to algos | ||
* @param microalgos number | ||
* @returns number | ||
*/ | ||
function microalgosToAlgos(microalgos) { | ||
if (microalgos < 0 || !Number.isSafeInteger(microalgos)){ | ||
throw ERROR_INVALID_MICROALGOS; | ||
} | ||
return microalgos/MICROALGOS_TO_ALGOS_RATIO | ||
} | ||
/** | ||
* algosToMicroalgos converts algos to microalgos | ||
* @param algos number | ||
* @returns number | ||
*/ | ||
function algosToMicroalgos(algos) { | ||
let microalgos = algos*MICROALGOS_TO_ALGOS_RATIO; | ||
return Math.round(microalgos) | ||
} | ||
module.exports = { | ||
@@ -216,2 +274,4 @@ isValidAddress, | ||
signBid, | ||
signBytes, | ||
verifyBytes, | ||
encodeObj, | ||
@@ -228,2 +288,5 @@ decodeObj, | ||
ERROR_MULTISIG_BAD_SENDER, | ||
ERROR_INVALID_MICROALGOS, | ||
microalgosToAlgos, | ||
algosToMicroalgos, | ||
}; |
@@ -33,4 +33,8 @@ const nacl = require('tweetnacl'); | ||
module.exports = {genericHash, randomBytes, keyPair, sign, keyPairFromSeed, keyPairFromSecretKey, bytesEqual}; | ||
function verify(message, signature, verifyKey) { | ||
return nacl.sign.detached.verify(message, signature, verifyKey); | ||
} | ||
module.exports = {genericHash, randomBytes, keyPair, sign, keyPairFromSeed, keyPairFromSecretKey, bytesEqual, verify}; | ||
// constants | ||
@@ -37,0 +41,0 @@ module.exports.PUBLIC_KEY_LENGTH = nacl.sign.publicKeyLength; |
@@ -14,3 +14,3 @@ const address = require("./encoding/address"); | ||
class Transaction { | ||
constructor({from, to, fee, amount, firstRound, lastRound, note, genesisID, genesisHash, closeRemainderTo}) { | ||
constructor({from, to, fee, amount, firstRound, lastRound, note, genesisID, genesisHash, closeRemainderTo, voteKey, selectionKey, voteFirst, voteLast, voteKeyDilution, type="pay", flatFee=false}) { | ||
this.name = "Transaction"; | ||
@@ -20,3 +20,3 @@ this.tag = Buffer.from([84, 88]); // "TX" | ||
from = address.decode(from); | ||
to = address.decode(to); | ||
if (to !== undefined) to = address.decode(to); | ||
@@ -29,3 +29,3 @@ if (closeRemainderTo !== undefined) closeRemainderTo = address.decode(closeRemainderTo); | ||
if (!Number.isSafeInteger(amount) || amount < 0) throw Error("Amount must be a positive number and smaller than 2^53-1"); | ||
if (amount !== undefined && (!Number.isSafeInteger(amount) || amount < 0)) throw Error("Amount must be a positive number and smaller than 2^53-1"); | ||
if (!Number.isSafeInteger(fee) || fee < 0) throw Error("fee must be a positive number and smaller than 2^53-1"); | ||
@@ -41,10 +41,17 @@ if (!Number.isSafeInteger(firstRound) || firstRound < 0) throw Error("firstRound must be a positive number"); | ||
} | ||
if (voteKey !== undefined) { | ||
voteKey = Buffer.from(voteKey, "base64"); | ||
} | ||
if (selectionKey !== undefined) { | ||
selectionKey = Buffer.from(selectionKey, "base64"); | ||
} | ||
Object.assign(this, { | ||
from, to, fee, amount, firstRound, lastRound, note, genesisHash, genesisID, closeRemainderTo | ||
from, to, fee, amount, firstRound, lastRound, note, genesisHash, genesisID, closeRemainderTo, voteKey, selectionKey, voteFirst, voteLast, voteKeyDilution, type | ||
}); | ||
// Modify Fee | ||
this.fee *= this.estimateSize(); | ||
if (!flatFee){ | ||
this.fee *= this.estimateSize(); | ||
} | ||
// If suggested fee too small and will be rejected, set to min tx fee | ||
@@ -57,25 +64,49 @@ if (this.fee < ALGORAND_MIN_TX_FEE) { | ||
get_obj_for_encoding() { | ||
let txn = { | ||
"amt": this.amount, | ||
"fee": this.fee, | ||
"fv": this.firstRound, | ||
"lv": this.lastRound, | ||
"note": Buffer.from(this.note), | ||
"rcv": Buffer.from(this.to.publicKey), | ||
"snd": Buffer.from(this.from.publicKey), | ||
"type": "pay", | ||
"gen": this.genesisID, | ||
"gh": this.genesisHash, | ||
}; | ||
if (this.type == "pay") { | ||
let txn = { | ||
"amt": this.amount, | ||
"fee": this.fee, | ||
"fv": this.firstRound, | ||
"lv": this.lastRound, | ||
"note": Buffer.from(this.note), | ||
"rcv": Buffer.from(this.to.publicKey), | ||
"snd": Buffer.from(this.from.publicKey), | ||
"type": "pay", | ||
"gen": this.genesisID, | ||
"gh": this.genesisHash, | ||
}; | ||
// parse close address | ||
if (this.closeRemainderTo !== undefined) txn.close = Buffer.from(this.closeRemainderTo.publicKey); | ||
// allowed zero values | ||
if (!txn.note.length) delete txn.note; | ||
if (!txn.amt) delete txn.amt; | ||
if (!txn.fee) delete txn.fee; | ||
if (!txn.gen) delete txn.gen; | ||
// parse close address | ||
if (this.closeRemainderTo !== undefined) txn.close = Buffer.from(this.closeRemainderTo.publicKey); | ||
// allowed zero values | ||
if (!txn.note.length) delete txn.note; | ||
if (!txn.amt) delete txn.amt; | ||
if (!txn.fee) delete txn.fee; | ||
if (!txn.gen) delete txn.gen; | ||
return txn; | ||
return txn; | ||
} | ||
else if (this.type == "keyreg") { | ||
let txn = { | ||
"fee": this.fee, | ||
"fv": this.firstRound, | ||
"lv": this.lastRound, | ||
"note": Buffer.from(this.note), | ||
"snd": Buffer.from(this.from.publicKey), | ||
"type": this.type, | ||
"gen": this.genesisID, | ||
"gh": this.genesisHash, | ||
"votekey": this.voteKey, | ||
"selkey": this.selectionKey, | ||
"votefst": this.voteFirst, | ||
"votelst": this.voteLast, | ||
"votekd": this.voteKeyDilution | ||
}; | ||
// allowed zero values | ||
if (!txn.note.length) delete txn.note; | ||
if (!txn.fee) delete txn.fee; | ||
if (!txn.gen) delete txn.gen; | ||
return txn; | ||
} | ||
} | ||
@@ -88,3 +119,5 @@ | ||
txn.amount = txnForEnc.amt; | ||
txn.genesisID = txnForEnc.gen; | ||
txn.genesisHash = txnForEnc.gh; | ||
txn.type = txnForEnc.type; | ||
txn.fee = txnForEnc.fee; | ||
@@ -94,7 +127,17 @@ txn.firstRound = txnForEnc.fv; | ||
txn.note = new Uint8Array(txnForEnc.note); | ||
txn.to = address.decode(address.encode(new Uint8Array(txnForEnc.rcv))); | ||
txn.from = address.decode(address.encode(new Uint8Array(txnForEnc.snd))); | ||
if (txnForEnc.close !== undefined) txn.closeRemainderTo = address.decode(address.encode(new Uint8Array(txnForEnc.close))); | ||
txn.genesisID = txnForEnc.gen; | ||
txn.genesisHash = txnForEnc.gh; | ||
if (txnForEnc.type === "pay") { | ||
txn.amount = txnForEnc.amt; | ||
txn.to = address.decode(address.encode(new Uint8Array(txnForEnc.rcv))); | ||
if (txnForEnc.close !== undefined) txn.closeRemainderTo = address.decode(address.encode(new Uint8Array(txnForEnc.close))); | ||
} | ||
else if (txnForEnc.type === "keyreg") { | ||
txn.voteKey = txnForEnc.votekey; | ||
txn.selectionKey = txnForEnc.selkey; | ||
txn.voteKeyDilution = txnForEnc.votekd; | ||
txn.voteFirst = txnForEnc.votefst; | ||
txn.voteLast = txnForEnc.votelst; | ||
} | ||
return txn; | ||
@@ -101,0 +144,0 @@ } |
12
test.js
const algosdk = require("./src/main"); | ||
console.log(algosdk.isValidAddress("HH3XQFKYAQK34WIH6X752FJY3ZVL5LSTFKC5ABLWMIA37YLVWFAAWZH6X4")); | ||
const params = { | ||
version: 1, | ||
threshold: 2, | ||
addrs: [ | ||
"NP3GVAWXXC7EEVOHSZIHSCDRO3I3ZCXXM5DWAAJLJHNQSJOV2CP5HWUWK4", | ||
"ZCFKADPCOJBKVJIXTKJMDYQGLULNEYEXPNXIZFW75XMHTQFETVAGICKB2Q", | ||
"ZIQKQM4ICEC7Z2CZEPKT4NZKHW7OCM3KNW3Q6J7D2IUXD2SWZ2X4GHHVHU", | ||
], | ||
}; | ||
let outAddr = algosdk.multisigAddress(params); | ||
console.log(outAddr) |
@@ -111,2 +111,49 @@ let assert = require('assert'); | ||
it('should correctly serialize and deserialize from msgpack representation with flat fee', function() { | ||
let o = { | ||
"from": "XMHLMNAVJIMAW2RHJXLXKKK4G3J3U6VONNO3BTAQYVDC3MHTGDP3J5OCRU", | ||
"to": "UCE2U2JC4O4ZR6W763GUQCG57HQCDZEUJY4J5I6VYY4HQZUJDF7AKZO5GM", | ||
"fee": 2063, | ||
"amount": 847, | ||
"firstRound": 51, | ||
"lastRound": 61, | ||
"note": new Uint8Array([123, 12, 200]), | ||
"genesisHash": "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=", | ||
"genesisID": "", | ||
"flatFee": true | ||
}; | ||
let expectedTxn = new transaction.Transaction(o); | ||
let encRep = expectedTxn.get_obj_for_encoding(); | ||
const encTxn = encoding.encode(encRep); | ||
const decEncRep = encoding.decode(encTxn); | ||
let decTxn = transaction.Transaction.from_obj_for_encoding(decEncRep); | ||
const reencRep = decTxn.get_obj_for_encoding(); | ||
assert.deepStrictEqual(reencRep, encRep); | ||
}); | ||
it('should correctly serialize and deserialize a key registration transaction from msgpack representation', function() { | ||
let o = { | ||
"from": "XMHLMNAVJIMAW2RHJXLXKKK4G3J3U6VONNO3BTAQYVDC3MHTGDP3J5OCRU", | ||
"fee": 10, | ||
"firstRound": 51, | ||
"lastRound": 61, | ||
"note": new Uint8Array([123, 12, 200]), | ||
"genesisHash": "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=", | ||
"voteKey": "5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKE=", | ||
"selectionKey": "oImqaSLjuZj63/bNSAjd+eAh5JROOJ6j1cY4eGaJGX4=", | ||
"voteFirst": 123, | ||
"voteLast": 456, | ||
"voteKeyDilution": 1234, | ||
"genesisID": "", | ||
"type": "keyreg" | ||
}; | ||
let expectedTxn = new transaction.Transaction(o); | ||
let encRep = expectedTxn.get_obj_for_encoding(); | ||
const encTxn = encoding.encode(encRep); | ||
const decEncRep = encoding.decode(encTxn); | ||
let decTxn = transaction.Transaction.from_obj_for_encoding(decEncRep); | ||
const reencRep = decTxn.get_obj_for_encoding(); | ||
assert.deepStrictEqual(reencRep, encRep); | ||
}); | ||
it('reserializes correctly no genesis ID', function() { | ||
@@ -113,0 +160,0 @@ let o = { |
@@ -57,4 +57,46 @@ let assert = require('assert'); | ||
}); | ||
it('should return a blob that matches the go code when using a flat fee', function () { | ||
let sk = "advice pudding treat near rule blouse same whisper inner electric quit surface sunny dismiss leader blood seat clown cost exist hospital century reform able sponsor"; | ||
let golden = "gqNzaWfEQPhUAZ3xkDDcc8FvOVo6UinzmKBCqs0woYSfodlmBMfQvGbeUx3Srxy3dyJDzv7rLm26BRv9FnL2/AuT7NYfiAWjdHhui6NhbXTNA+ilY2xvc2XEIEDpNJKIJWTLzpxZpptnVCaJ6aHDoqnqW2Wm6KRCH/xXo2ZlZc0EmKJmds0wsqNnZW6sZGV2bmV0LXYzMy4womdoxCAmCyAJoJOohot5WHIvpeVG7eftF+TYXEx4r7BFJpDt0qJsds00mqRub3RlxAjqABVHQ2y/lqNyY3bEIHts4k/rW6zAsWTinCIsV/X2PcOH1DkEglhBHF/hD3wCo3NuZMQg5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKGkdHlwZaNwYXk="; | ||
let o = { | ||
"to": "PNWOET7LLOWMBMLE4KOCELCX6X3D3Q4H2Q4QJASYIEOF7YIPPQBG3YQ5YI", | ||
"fee": 1176, | ||
"amount": 1000, | ||
"firstRound": 12466, | ||
"lastRound": 13466, | ||
"genesisID": "devnet-v33.0", | ||
"genesisHash": "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=", | ||
"closeRemainderTo": "IDUTJEUIEVSMXTU4LGTJWZ2UE2E6TIODUKU6UW3FU3UKIQQ77RLUBBBFLA", | ||
"note": new Uint8Array(Buffer.from("6gAVR0Nsv5Y=", "base64")), | ||
"flatFee": true | ||
}; | ||
sk = algosdk.mnemonicToSecretKey(sk); | ||
let js_dec = algosdk.signTransaction(o, sk.sk); | ||
assert.deepStrictEqual(Buffer.from(js_dec.blob), Buffer.from(golden, "base64")); | ||
// // Check txid | ||
let tx_golden = "5FJDJD5LMZC3EHUYYJNH5I23U4X6H2KXABNDGPIL557ZMJ33GZHQ"; | ||
assert.deepStrictEqual(js_dec.txID, tx_golden); | ||
}); | ||
}); | ||
describe('Sign and verify bytes', function () { | ||
it('should verify a correct signature', function () { | ||
let account = algosdk.generateAccount(); | ||
let toSign = new Uint8Array(Buffer.from([1, 9, 25, 49])); | ||
let signed = algosdk.signBytes(toSign, account.sk); | ||
assert.equal(true, algosdk.verifyBytes(toSign, signed, account.addr)) | ||
}); | ||
it('should not verify a corrupted signature', function () { | ||
let account = algosdk.generateAccount(); | ||
let toSign = Buffer.from([1, 9, 25, 49]); | ||
let signed = algosdk.signBytes(toSign, account.sk); | ||
signed[0] = (signed[0] + 1)%256; | ||
assert.equal(false, algosdk.verifyBytes(toSign, signed, account.addr)) | ||
}); | ||
}); | ||
describe('Multisig Sign', function () { | ||
@@ -61,0 +103,0 @@ it('should return a blob that matches the go code', function () { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
505622
30
4467