diff --git a/src/scval.js b/src/scval.js index 67bf0e62..259192c0 100644 --- a/src/scval.js +++ b/src/scval.js @@ -172,13 +172,6 @@ export function nativeToScVal(val, opts = {}) { } if (Array.isArray(val)) { - if (val.length > 0 && val.some((v) => typeof v !== typeof val[0])) { - throw new TypeError( - `array values (${val}) must have the same type (types: ${val - .map((v) => typeof v) - .join(',')})` - ); - } return xdr.ScVal.scvVec(val.map((v) => nativeToScVal(v, opts))); } @@ -383,3 +376,27 @@ export function scValToNative(scv) { return scv.value(); } } + +/// Inject a sortable map builder into the xdr module. +xdr.scvSortedMap = (items) => { + const sorted = Array.from(items).sort((a, b) => { + // Both a and b are `ScMapEntry`s, so we need to sort by underlying key. + // + // We couldn't possibly handle every combination of keys since Soroban + // maps don't enforce consistent types, so we do a best-effort and try + // sorting by "number-like" or "string-like." + const nativeA = scValToNative(a.key()); + const nativeB = scValToNative(b.key()); + + switch (typeof nativeA) { + case 'number': + case 'bigint': + return nativeA < nativeB ? -1 : 1; + + default: + return nativeA.toString().localeCompare(nativeB.toString()); + } + }); + + return xdr.ScVal.scvMap(sorted); +}; diff --git a/test/unit/scval_test.js b/test/unit/scval_test.js index c62a0e90..9bd648ee 100644 --- a/test/unit/scval_test.js +++ b/test/unit/scval_test.js @@ -81,8 +81,6 @@ describe('parsing and building ScVals', function () { // iterate for granular errors on failures targetScv.value().forEach((entry, idx) => { const actual = scv.value()[idx]; - // console.log(idx, 'exp:', JSON.stringify(entry)); - // console.log(idx, 'act:', JSON.stringify(actual)); expect(entry).to.deep.equal(actual, `item ${idx} doesn't match`); }); @@ -207,8 +205,8 @@ describe('parsing and building ScVals', function () { ); }); - it('throws on arrays with mixed types', function () { - expect(() => nativeToScVal([1, 'a', false])).to.throw(/same type/i); + it('doesnt throw on arrays with mixed types', function () { + expect(nativeToScVal([1, 'a', false]).switch().name).to.equal('scvVec'); }); it('lets strings be small integer ScVals', function () { @@ -264,4 +262,71 @@ describe('parsing and building ScVals', function () { } ]); }); + + it('can sort maps by string', function () { + const sample = nativeToScVal( + { a: 1, b: 2, c: 3 }, + { + type: { + a: ['symbol'], + b: ['symbol'], + c: ['symbol'] + } + } + ); + ['a', 'b', 'c'].forEach((val, idx) => { + expect(sample.value()[idx].key().value()).to.equal(val); + }); + + // nativeToScVal will sort, so we need to "unsort" to make sure it works. + // We'll do this by swapping 0 (a) and 2 (c). + let tmp = sample.value()[0]; + sample.value()[0] = sample.value()[2]; + sample.value()[2] = tmp; + + ['c', 'b', 'a'].forEach((val, idx) => { + expect(sample.value()[idx].key().value()).to.equal(val); + }); + + const sorted = xdr.scvSortedMap(sample.value()); + expect(sorted.switch().name).to.equal('scvMap'); + ['a', 'b', 'c'].forEach((val, idx) => { + expect(sorted.value()[idx].key().value()).to.equal(val); + }); + }); + + it('can sort number-like maps', function () { + const sample = nativeToScVal( + { 1: 'a', 2: 'b', 3: 'c' }, + { + type: { + 1: ['i64', 'symbol'], + 2: ['i64', 'symbol'], + 3: ['i64', 'symbol'] + } + } + ); + expect(sample.value()[0].key().switch().name).to.equal('scvI64'); + + [1n, 2n, 3n].forEach((val, idx) => { + let underlyingKey = sample.value()[idx].key().value(); + expect(underlyingKey.toBigInt()).to.equal(val); + }); + + // nativeToScVal will sort, so we need to "unsort" to make sure it works. + // We'll do this by swapping 0th (1n) and 2nd (3n). + let tmp = sample.value()[0]; + sample.value()[0] = sample.value()[2]; + sample.value()[2] = tmp; + + [3n, 2n, 1n].forEach((val, idx) => { + expect(sample.value()[idx].key().value().toBigInt()).to.equal(val); + }); + + const sorted = xdr.scvSortedMap(sample.value()); + expect(sorted.switch().name).to.equal('scvMap'); + [1n, 2n, 3n].forEach((val, idx) => { + expect(sorted.value()[idx].key().value().toBigInt()).to.equal(val); + }); + }); }); diff --git a/types/curr.d.ts b/types/curr.d.ts index beafaa43..00be76e4 100644 --- a/types/curr.d.ts +++ b/types/curr.d.ts @@ -44,6 +44,16 @@ export namespace xdr { type Hash = Opaque[]; // workaround, cause unknown + /** + * Returns an {@link ScVal} with a map type and sorted entries. + * + * @param items the key-value pairs to sort. + * + * @warning This only performs "best-effort" sorting, working best when the + * keys are all either numeric or string-like. + */ + function scvSortedMap(items: ScMapEntry[]): ScVal; + interface SignedInt { readonly MAX_VALUE: 2147483647; readonly MIN_VALUE: -2147483648;