Skip to content

Commit ef3fb70

Browse files
authored
Added support for mixed cadence imports (#2669)
* Added support for mixed cadence imports * Updated with safer backward compatible check * Prettier fix --------- Co-authored-by: mfbz <mfbz@users.noreply.github.com>
1 parent 879ae91 commit ef3fb70

File tree

3 files changed

+219
-4
lines changed

3 files changed

+219
-4
lines changed

.changeset/ready-lands-marry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@onflow/sdk": minor
3+
---
4+
5+
Added support for mixed cadence imports

packages/sdk/src/resolve/resolve-cadence.test.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,4 +229,201 @@ access(all) fun main(): Address {
229229
expect(ix.message.cadence).toEqual(expected)
230230
})
231231
})
232+
233+
describe("mixed import syntax", () => {
234+
test("supports both string imports and traditional imports", async () => {
235+
const CADENCE = `import "FungibleToken"
236+
import MyContract from 0x12345678
237+
238+
access(all) fun main(): Address {
239+
return 0x12345678
240+
}`
241+
242+
const expected = `import FungibleToken from 0xf233dcee88fe0abe
243+
import MyContract from 0x12345678
244+
245+
access(all) fun main(): Address {
246+
return 0x12345678
247+
}`
248+
249+
await config().put("system.contracts.FungibleToken", "0xf233dcee88fe0abe")
250+
await idle()
251+
252+
const ix = await pipe([
253+
makeScript,
254+
put("ix.cadence", CADENCE),
255+
async ix => resolveCadence(ix, await getGlobalContext()),
256+
])(initInteraction())
257+
258+
expect(ix.message.cadence).toEqual(expected)
259+
})
260+
261+
test("supports multiple string imports with traditional imports", async () => {
262+
const CADENCE = `import "FungibleToken"
263+
import "NonFungibleToken"
264+
import MyContract from 0xABCDEF
265+
import FlowToken from 0x0ae53cb6e3f42a79
266+
267+
access(all) fun main(): String {
268+
return "mixed imports work"
269+
}`
270+
271+
const expected = `import FungibleToken from 0xf233dcee88fe0abe
272+
import NonFungibleToken from 0x1d7e57aa55817448
273+
import MyContract from 0xABCDEF
274+
import FlowToken from 0x0ae53cb6e3f42a79
275+
276+
access(all) fun main(): String {
277+
return "mixed imports work"
278+
}`
279+
280+
await config().put("system.contracts.FungibleToken", "0xf233dcee88fe0abe")
281+
await config().put(
282+
"system.contracts.NonFungibleToken",
283+
"0x1d7e57aa55817448"
284+
)
285+
await idle()
286+
287+
const ix = await pipe([
288+
makeScript,
289+
put("ix.cadence", CADENCE),
290+
async ix => resolveCadence(ix, await getGlobalContext()),
291+
])(initInteraction())
292+
293+
expect(ix.message.cadence).toEqual(expected)
294+
})
295+
296+
test("traditional imports with explicit addresses should not be modified", async () => {
297+
const CADENCE = `import FlowToken from 0x7e60df042a9c0868
298+
import MyContract from 0x1234567890abcdef
299+
300+
access(all) fun main(): UFix64 {
301+
return 42.0
302+
}`
303+
304+
const expected = `import FlowToken from 0x7e60df042a9c0868
305+
import MyContract from 0x1234567890abcdef
306+
307+
access(all) fun main(): UFix64 {
308+
return 42.0
309+
}`
310+
311+
// No config needed - explicit addresses should work as-is
312+
const ix = await pipe([
313+
makeScript,
314+
put("ix.cadence", CADENCE),
315+
async ix => resolveCadence(ix, await getGlobalContext()),
316+
])(initInteraction())
317+
318+
expect(ix.message.cadence).toEqual(expected)
319+
})
320+
321+
test("string import without config should log warning and leave import unchanged", async () => {
322+
const CADENCE = `import "UnconfiguredContract"
323+
import FlowToken from 0x7e60df042a9c0868
324+
325+
access(all) fun main(): Bool {
326+
return true
327+
}`
328+
329+
const expected = `import "UnconfiguredContract"
330+
import FlowToken from 0x7e60df042a9c0868
331+
332+
access(all) fun main(): Bool {
333+
return true
334+
}`
335+
336+
// Spy on console.warn to verify warning is logged
337+
const warnSpy = jest.spyOn(console, "warn").mockImplementation()
338+
339+
const ix = await pipe([
340+
makeScript,
341+
put("ix.cadence", CADENCE),
342+
async ix => resolveCadence(ix, await getGlobalContext()),
343+
])(initInteraction())
344+
345+
expect(ix.message.cadence).toEqual(expected)
346+
347+
// Verify warning was logged
348+
expect(warnSpy).toHaveBeenCalled()
349+
const warnCall = warnSpy.mock.calls.find(call =>
350+
call.join(" ").includes("Contract Placeholder not found")
351+
)
352+
expect(warnCall).toBeDefined()
353+
expect(warnCall?.join(" ")).toContain("UnconfiguredContract")
354+
355+
warnSpy.mockRestore()
356+
})
357+
358+
test("legacy placeholders and string imports together should throw invariant error", async () => {
359+
const CADENCE = `import "FungibleToken"
360+
import FlowToken from 0xFLOWTOKEN
361+
362+
access(all) fun main(): Bool {
363+
return true
364+
}`
365+
366+
config().put("system.contracts.FungibleToken", "0xf233dcee88fe0abe")
367+
config().put("0xFLOWTOKEN", "0x7e60df042a9c0868")
368+
await idle()
369+
370+
await expect(async () => {
371+
await pipe([
372+
makeScript,
373+
put("ix.cadence", CADENCE),
374+
async ix => resolveCadence(ix, await getGlobalContext()),
375+
])(initInteraction())
376+
}).rejects.toThrow(
377+
"Both account identifier and contract identifier syntax not simultaneously supported."
378+
)
379+
})
380+
381+
test("legacy placeholders alone should work without string imports", async () => {
382+
const CADENCE = `import FlowToken from 0xFLOWTOKEN
383+
import MyContract from 0xMYCONTRACT
384+
385+
access(all) fun main(): Address {
386+
return 0xFLOWTOKEN
387+
}`
388+
389+
const expected = `import FlowToken from 0x7e60df042a9c0868
390+
import MyContract from 0x1234567890abcdef
391+
392+
access(all) fun main(): Address {
393+
return 0x7e60df042a9c0868
394+
}`
395+
396+
config().put("0xFLOWTOKEN", "0x7e60df042a9c0868")
397+
config().put("0xMYCONTRACT", "0x1234567890abcdef")
398+
await idle()
399+
400+
const ix = await pipe([
401+
makeScript,
402+
put("ix.cadence", CADENCE),
403+
async ix => resolveCadence(ix, await getGlobalContext()),
404+
])(initInteraction())
405+
406+
expect(ix.message.cadence).toEqual(expected)
407+
})
408+
})
409+
410+
describe("no imports", () => {
411+
test("cadence with no imports should remain unchanged", async () => {
412+
const CADENCE = `access(all) fun main(): String {
413+
return "Hello, Flow!"
414+
}`
415+
416+
const expected = `access(all) fun main(): String {
417+
return "Hello, Flow!"
418+
}`
419+
420+
const ix = await pipe([
421+
makeScript,
422+
put("ix.cadence", CADENCE),
423+
async ix => resolveCadence(ix, await getGlobalContext()),
424+
])(initInteraction())
425+
426+
expect(ix.message.cadence).toEqual(expected)
427+
})
428+
})
232429
})

packages/sdk/src/resolve/resolve-cadence.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,19 @@ const isFn = (v: any): v is Function => typeof v === "function"
1010
const isString = (v: any): v is string => typeof v === "string"
1111

1212
const oldIdentifierPatternFn = (): RegExp => /\b(0x\w+)\b/g
13-
function isOldIdentifierSyntax(cadence: string): boolean {
14-
return oldIdentifierPatternFn().test(cadence)
13+
function isOldIdentifierSyntax(
14+
cadence: string,
15+
legacyContractIdentifiers: Record<string, string> = {}
16+
): boolean {
17+
const matches = cadence.matchAll(oldIdentifierPatternFn())
18+
for (const match of matches) {
19+
const identifier = match[0]
20+
// Only return true if we have a legacy identifier that needs replacement
21+
if (legacyContractIdentifiers[identifier]) {
22+
return true
23+
}
24+
}
25+
return false
1526
}
1627

1728
const newIdentifierPatternFn = (): RegExp => /import\s+"(\w+)"/g
@@ -38,10 +49,12 @@ export function createResolveCadence(context: SdkContext) {
3849
if (isFn(cadence)) cadence = await cadence({} as Record<string, never>)
3950
invariant(isString(cadence), "Cadence needs to be a string at this point.")
4051
invariant(
41-
!isOldIdentifierSyntax(cadence) || !isNewIdentifierSyntax(cadence),
52+
!isOldIdentifierSyntax(cadence, context.legacyContractIdentifiers) ||
53+
!isNewIdentifierSyntax(cadence),
4254
"Both account identifier and contract identifier syntax not simultaneously supported."
4355
)
44-
if (isOldIdentifierSyntax(cadence)) {
56+
57+
if (isOldIdentifierSyntax(cadence, context.legacyContractIdentifiers)) {
4558
cadence = Object.entries(context.legacyContractIdentifiers || {}).reduce(
4659
(cadence, [key, value]) => {
4760
const regex = new RegExp("(\\b" + key + "\\b)", "g")

0 commit comments

Comments
 (0)