From cf98a66e5a3f8c041573f1299a155ec810a21022 Mon Sep 17 00:00:00 2001 From: George Date: Thu, 16 Jan 2025 09:00:46 -0800 Subject: [PATCH 1/5] Remove 'same type vector' requirement --- src/scval.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/scval.js b/src/scval.js index 67bf0e62..605b81b8 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))); } From 2650f85206c152ff3e48f450e2ee93802783b914 Mon Sep 17 00:00:00 2001 From: George Date: Thu, 16 Jan 2025 09:02:34 -0800 Subject: [PATCH 2/5] Add sorted variation for ScMap creation --- src/xdr.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/xdr.js b/src/xdr.js index d97dbe29..95dc4c55 100644 --- a/src/xdr.js +++ b/src/xdr.js @@ -1,3 +1,25 @@ import xdr from './generated/curr_generated'; +import { scValToNative } from './scval'; + +xdr.scvMapSorted = (items) => { + return xdr.ScVal.scvMap(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." + let nativeA = scValToNative(a.key()), + nativeB = scValToNative(b.key()); + + switch (typeof nativeA) { + case "number": + case "bigint": + return nativeA < nativeB; + + default: + return nativeA.toString().localeCompare(nativeB.toString()); + } + })); +} export default xdr; From 9402f35ccf2c1002bb4937df2111852dfcf08c73 Mon Sep 17 00:00:00 2001 From: George Date: Thu, 16 Jan 2025 09:47:29 -0800 Subject: [PATCH 3/5] Add tests+fixes for string/number cases --- src/xdr.js | 36 ++++++++++---------- test/unit/scval_test.js | 73 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 21 deletions(-) diff --git a/src/xdr.js b/src/xdr.js index 95dc4c55..3f53d3f2 100644 --- a/src/xdr.js +++ b/src/xdr.js @@ -2,24 +2,26 @@ import xdr from './generated/curr_generated'; import { scValToNative } from './scval'; xdr.scvMapSorted = (items) => { - return xdr.ScVal.scvMap(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." - let nativeA = scValToNative(a.key()), - nativeB = scValToNative(b.key()); + let 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." + let nativeA = scValToNative(a.key()), + nativeB = scValToNative(b.key()); - switch (typeof nativeA) { - case "number": - case "bigint": - return nativeA < nativeB; + switch (typeof nativeA) { + case 'number': + case 'bigint': + return nativeA < nativeB ? -1 : 1; - default: - return nativeA.toString().localeCompare(nativeB.toString()); - } - })); -} + default: + return nativeA.toString().localeCompare(nativeB.toString()); + } + }); + + return xdr.ScVal.scvMap(sorted); +}; export default xdr; diff --git a/test/unit/scval_test.js b/test/unit/scval_test.js index c62a0e90..37089414 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.scvMapSorted(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.scvMapSorted(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); + }); + }); }); From 267851845750b5bfe37020f6af5fcc9ec20cf868 Mon Sep 17 00:00:00 2001 From: George Date: Thu, 16 Jan 2025 09:53:46 -0800 Subject: [PATCH 4/5] Rename, add typescript, move to avoid dep cycle --- src/scval.js | 24 ++++++++++++++++++++++++ src/xdr.js | 24 ------------------------ test/unit/scval_test.js | 4 ++-- types/curr.d.ts | 10 ++++++++++ 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/scval.js b/src/scval.js index 605b81b8..725ab004 100644 --- a/src/scval.js +++ b/src/scval.js @@ -376,3 +376,27 @@ export function scValToNative(scv) { return scv.value(); } } + +/// Inject a sortable map builder into the xdr module. +xdr.scvSortedMap = (items) => { + let 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." + let nativeA = scValToNative(a.key()), + 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/src/xdr.js b/src/xdr.js index 3f53d3f2..d97dbe29 100644 --- a/src/xdr.js +++ b/src/xdr.js @@ -1,27 +1,3 @@ import xdr from './generated/curr_generated'; -import { scValToNative } from './scval'; - -xdr.scvMapSorted = (items) => { - let 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." - let nativeA = scValToNative(a.key()), - 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); -}; export default xdr; diff --git a/test/unit/scval_test.js b/test/unit/scval_test.js index 37089414..9bd648ee 100644 --- a/test/unit/scval_test.js +++ b/test/unit/scval_test.js @@ -288,7 +288,7 @@ describe('parsing and building ScVals', function () { expect(sample.value()[idx].key().value()).to.equal(val); }); - const sorted = xdr.scvMapSorted(sample.value()); + 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); @@ -323,7 +323,7 @@ describe('parsing and building ScVals', function () { expect(sample.value()[idx].key().value().toBigInt()).to.equal(val); }); - const sorted = xdr.scvMapSorted(sample.value()); + 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; From e77bb26492adc6d4a886324cedd6781556af67da Mon Sep 17 00:00:00 2001 From: George Date: Thu, 16 Jan 2025 10:06:55 -0800 Subject: [PATCH 5/5] Fix eslint whining --- src/scval.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scval.js b/src/scval.js index 725ab004..259192c0 100644 --- a/src/scval.js +++ b/src/scval.js @@ -379,14 +379,14 @@ export function scValToNative(scv) { /// Inject a sortable map builder into the xdr module. xdr.scvSortedMap = (items) => { - let sorted = Array.from(items).sort((a, b) => { + 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." - let nativeA = scValToNative(a.key()), - nativeB = scValToNative(b.key()); + const nativeA = scValToNative(a.key()); + const nativeB = scValToNative(b.key()); switch (typeof nativeA) { case 'number':