diff --git a/crates/sui-graphql-e2e-tests/tests/consistency/dynamic_fields/dynamic_fields.exp b/crates/sui-graphql-e2e-tests/tests/consistency/dynamic_fields/dynamic_fields.exp index a5130682191d9..4cde77acbb206 100644 --- a/crates/sui-graphql-e2e-tests/tests/consistency/dynamic_fields/dynamic_fields.exp +++ b/crates/sui-graphql-e2e-tests/tests/consistency/dynamic_fields/dynamic_fields.exp @@ -193,7 +193,7 @@ Response: { "contents": { "json": { "id": "0x844185d7b145f503838a1d509845a40ee249534f723dd2f003d9efdfc581000d", - "count": "1" + "count": "2" } } } @@ -313,7 +313,7 @@ Response: { "contents": { "json": { "id": "0x844185d7b145f503838a1d509845a40ee249534f723dd2f003d9efdfc581000d", - "count": "1" + "count": "2" } } } @@ -411,7 +411,7 @@ Response: { "contents": { "json": { "id": "0x844185d7b145f503838a1d509845a40ee249534f723dd2f003d9efdfc581000d", - "count": "1" + "count": "2" } } } @@ -540,7 +540,7 @@ Response: { "contents": { "json": { "id": "0x844185d7b145f503838a1d509845a40ee249534f723dd2f003d9efdfc581000d", - "count": "1" + "count": "2" } } } @@ -664,7 +664,7 @@ Response: { "contents": { "json": { "id": "0x844185d7b145f503838a1d509845a40ee249534f723dd2f003d9efdfc581000d", - "count": "1" + "count": "2" } } } @@ -765,7 +765,7 @@ Response: { "contents": { "json": { "id": "0x844185d7b145f503838a1d509845a40ee249534f723dd2f003d9efdfc581000d", - "count": "1" + "count": "2" } } } @@ -831,7 +831,14 @@ Response: { "repr": "u64" } }, - "value": null + "value": { + "contents": { + "json": { + "id": "0x844185d7b145f503838a1d509845a40ee249534f723dd2f003d9efdfc581000d", + "count": "2" + } + } + } } } ] @@ -892,7 +899,14 @@ Response: { "repr": "u64" } }, - "value": null + "value": { + "contents": { + "json": { + "id": "0x844185d7b145f503838a1d509845a40ee249534f723dd2f003d9efdfc581000d", + "count": "2" + } + } + } } } ] diff --git a/crates/sui-graphql-e2e-tests/tests/consistency/dynamic_fields/immutable_dof.exp b/crates/sui-graphql-e2e-tests/tests/consistency/dynamic_fields/immutable_dof.exp new file mode 100644 index 0000000000000..04c4c2558280f --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/consistency/dynamic_fields/immutable_dof.exp @@ -0,0 +1,204 @@ +processed 17 tasks + +init: +A: object(0,0) + +task 1 'publish'. lines 20-68: +created: object(1,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 8770400, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 70-70: +created: object(2,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 3 'run'. lines 72-72: +created: object(3,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2295200, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 4 'run'. lines 74-74: +created: object(4,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2295200, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 5 'run'. lines 76-76: +created: object(5,0) +mutated: object(0,0), object(2,0), object(3,0) +gas summary: computation_cost: 1000000, storage_cost: 6064800, storage_rebate: 3573900, non_refundable_storage_fee: 36100 + +task 6 'run'. lines 78-78: +created: object(6,0) +mutated: object(0,0), object(2,0), object(4,0) +gas summary: computation_cost: 1000000, storage_cost: 6064800, storage_rebate: 3573900, non_refundable_storage_fee: 36100 + +task 7 'run'. lines 80-80: +mutated: object(0,0), object(2,0), object(3,0) +deleted: object(5,0) +gas summary: computation_cost: 1000000, storage_cost: 3610000, storage_rebate: 6004152, non_refundable_storage_fee: 60648 + +task 8 'create-checkpoint'. lines 82-82: +Checkpoint created: 1 + +task 9 'run-graphql'. lines 84-114: +Response: { + "data": { + "object": { + "dynamicFields": { + "nodes": [ + { + "value": { + "address": "0x0ff6cdb59f761881b9ad479af854ca71c4f560a229795178771459a6522c57f0", + "version": 5, + "contents": { + "json": { + "id": "0x0ff6cdb59f761881b9ad479af854ca71c4f560a229795178771459a6522c57f0", + "count": "0" + } + }, + "dynamicFields": { + "nodes": [] + } + } + } + ] + } + } + } +} + +task 10 'run-graphql'. lines 116-146: +Response: { + "data": { + "object": { + "dynamicFields": { + "nodes": [ + { + "value": { + "address": "0x0ff6cdb59f761881b9ad479af854ca71c4f560a229795178771459a6522c57f0", + "version": 5, + "contents": { + "json": { + "id": "0x0ff6cdb59f761881b9ad479af854ca71c4f560a229795178771459a6522c57f0", + "count": "0" + } + }, + "dynamicFields": { + "nodes": [ + { + "value": { + "address": "0xb1585cc21bb75b2e89747d4db64e0d73b227084d33b12f5715d1764a94c6b601", + "version": 6, + "contents": { + "json": { + "id": "0xb1585cc21bb75b2e89747d4db64e0d73b227084d33b12f5715d1764a94c6b601", + "count": "0" + } + } + } + } + ] + } + } + } + ] + } + } + } +} + +task 11 'run-graphql'. lines 148-178: +Response: { + "data": { + "object": { + "dynamicFields": { + "nodes": [] + } + } + } +} + +task 12 'run-graphql'. lines 180-210: +Response: { + "data": { + "object": null + } +} + +task 13 'run-graphql'. lines 212-239: +Response: { + "data": { + "object": { + "owner": { + "parent": { + "address": "0x6642719611d6990381dc19edcc37a7d353a1ea55281bf8702d0ad8bf3cfb7ea4" + } + }, + "dynamicFields": { + "nodes": [] + } + } + } +} + +task 14 'run-graphql'. lines 241-258: +Response: { + "data": { + "object": null + } +} + +task 15 'run-graphql'. lines 260-287: +Response: { + "data": { + "object": { + "owner": { + "_": null + }, + "dynamicFields": { + "nodes": [ + { + "value": { + "address": "0xb1585cc21bb75b2e89747d4db64e0d73b227084d33b12f5715d1764a94c6b601", + "version": 6, + "contents": { + "json": { + "id": "0xb1585cc21bb75b2e89747d4db64e0d73b227084d33b12f5715d1764a94c6b601", + "count": "0" + } + } + } + } + ] + } + } + } +} + +task 16 'run-graphql'. lines 289-316: +Response: { + "data": { + "object": { + "owner": { + "_": null + }, + "dynamicFields": { + "nodes": [ + { + "value": { + "address": "0xb1585cc21bb75b2e89747d4db64e0d73b227084d33b12f5715d1764a94c6b601", + "version": 6, + "contents": { + "json": { + "id": "0xb1585cc21bb75b2e89747d4db64e0d73b227084d33b12f5715d1764a94c6b601", + "count": "0" + } + } + } + } + ] + } + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/consistency/dynamic_fields/immutable_dof.move b/crates/sui-graphql-e2e-tests/tests/consistency/dynamic_fields/immutable_dof.move new file mode 100644 index 0000000000000..05f1f5ffe0b49 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/consistency/dynamic_fields/immutable_dof.move @@ -0,0 +1,316 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// 1. create Parent1 (2) +// 2. create Child1 (3) +// 3. create Child2 (4) +// 4. add Child1 to Parent1 -> Parent1 (5), Child1 (5) +// 5. add Child2 as a nested child object by borrowing Parent1.Child1 -> Parent1 (6), Child1 (5), Child2 (6) +// 6. make Child1 immutable + +// dynamic fields rooted on parent +// Child1 (5) should have a parent +// Child1 (6) does not exist +// Child1 (7) should show as an Immutable object + +// Verify that Parent1 (6) -> Child1 (5) -> Child2 (6) + +//# init --protocol-version 44 --addresses Test=0x0 --accounts A --simulator + +//# publish +module Test::M1 { + use sui::dynamic_object_field as ofield; + + public struct Parent has key, store { + id: UID, + count: u64 + } + + public struct Child has key, store { + id: UID, + count: u64, + } + + public entry fun parent(recipient: address, ctx: &mut TxContext) { + transfer::public_transfer( + Parent { id: object::new(ctx), count: 0 }, + recipient + ) + } + + public entry fun mutate_parent(parent: &mut Parent) { + parent.count = parent.count + 42; + } + + public entry fun child(recipient: address, ctx: &mut TxContext) { + transfer::public_transfer( + Child { id: object::new(ctx), count: 0 }, + recipient + ) + } + + public fun add_child(parent: &mut Parent, child: Child, name: u64) { + ofield::add(&mut parent.id, name, child); + } + + public fun add_nested_child(parent: &mut Parent, child_name: u64, nested_child: Child, nested_child_name: u64) { + let child: &mut Child = ofield::borrow_mut(&mut parent.id, child_name); + ofield::add(&mut child.id, nested_child_name, nested_child); + } + + public fun reclaim_child(parent: &mut Parent, name: u64): Child { + ofield::remove(&mut parent.id, name) + } + + public fun reclaim_and_freeze_child(parent: &mut Parent, name: u64) { + transfer::public_freeze_object(reclaim_child(parent, name)) + } +} + +//# run Test::M1::parent --sender A --args @A + +//# run Test::M1::child --sender A --args @A + +//# run Test::M1::child --sender A --args @A + +//# run Test::M1::add_child --sender A --args object(2,0) object(3,0) 42 + +//# run Test::M1::add_nested_child --sender A --args object(2,0) 42 object(4,0) 420 + +//# run Test::M1::reclaim_and_freeze_child --sender A --args object(2,0) 42 + +//# create-checkpoint + +//# run-graphql +{ + object(address: "@{obj_2_0}", version: 5) { + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + } + } + } + } + } + } + } + } + } +} + +//# run-graphql +{ + object(address: "@{obj_2_0}", version: 6) { + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + } + } + } + } + } + } + } + } + } +} + +//# run-graphql +{ + object(address: "@{obj_2_0}", version: 7) { + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + } + } + } + } + } + } + } + } + } +} + +//# run-graphql +{ + object(address: "@{obj_2_0}", version: 8) { + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + } + } + } + } + } + } + } + } + } +} + +//# run-graphql +{ + object(address: "@{obj_3_0}", version: 5) { + owner { + ... on Immutable { + _ + } + ... on Parent { + parent { + address + } + } + } + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + } + } + } + } + } +} + +//# run-graphql +{ + object(address: "@{obj_3_0}", version: 6) { + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + } + } + } + } + } +} + +//# run-graphql +{ + object(address: "@{obj_3_0}", version: 7) { + owner { + ... on Immutable { + _ + } + ... on Parent { + parent { + address + } + } + } + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + } + } + } + } + } +} + +//# run-graphql +{ + object(address: "@{obj_3_0}") { + owner { + ... on Immutable { + _ + } + ... on Parent { + parent { + address + } + } + } + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + } + } + } + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/consistency/dynamic_fields/nested_dof.exp b/crates/sui-graphql-e2e-tests/tests/consistency/dynamic_fields/nested_dof.exp new file mode 100644 index 0000000000000..13a83492198a8 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/consistency/dynamic_fields/nested_dof.exp @@ -0,0 +1,234 @@ +processed 17 tasks + +init: +A: object(0,0) + +task 1 'publish'. lines 24-75: +created: object(1,0) +mutated: object(0,1) +gas summary: computation_cost: 1000000, storage_cost: 8960400, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 2 'run'. lines 77-77: +created: object(2,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2302800, storage_rebate: 0, non_refundable_storage_fee: 0 + +task 3 'run'. lines 79-79: +created: object(3,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2295200, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 4 'run'. lines 81-81: +created: object(4,0) +mutated: object(0,0) +gas summary: computation_cost: 1000000, storage_cost: 2295200, storage_rebate: 978120, non_refundable_storage_fee: 9880 + +task 5 'run'. lines 83-83: +created: object(5,0) +mutated: object(0,0), object(2,0), object(3,0) +gas summary: computation_cost: 1000000, storage_cost: 6064800, storage_rebate: 3573900, non_refundable_storage_fee: 36100 + +task 6 'run'. lines 85-85: +created: object(6,0) +mutated: object(0,0), object(2,0), object(4,0) +gas summary: computation_cost: 1000000, storage_cost: 6064800, storage_rebate: 3573900, non_refundable_storage_fee: 36100 + +task 7 'run'. lines 87-87: +mutated: object(0,0), object(2,0), object(3,0) +gas summary: computation_cost: 1000000, storage_cost: 3610000, storage_rebate: 3573900, non_refundable_storage_fee: 36100 + +task 8 'run'. lines 89-89: +mutated: object(0,0), object(2,0), object(4,0) +gas summary: computation_cost: 1000000, storage_cost: 3610000, storage_rebate: 3573900, non_refundable_storage_fee: 36100 + +task 9 'create-checkpoint'. lines 91-91: +Checkpoint created: 1 + +task 10 'run-graphql'. lines 93-123: +Response: { + "data": { + "object": { + "dynamicFields": { + "nodes": [ + { + "value": { + "address": "0x7cfc2f4e6743c28eee330e37ae5293166f2e9e397e38cda3964d50abf3efb626", + "version": 5, + "contents": { + "json": { + "id": "0x7cfc2f4e6743c28eee330e37ae5293166f2e9e397e38cda3964d50abf3efb626", + "count": "0" + } + }, + "dynamicFields": { + "nodes": [] + } + } + } + ] + } + } + } +} + +task 11 'run-graphql'. lines 125-155: +Response: { + "data": { + "object": { + "dynamicFields": { + "nodes": [ + { + "value": { + "address": "0x7cfc2f4e6743c28eee330e37ae5293166f2e9e397e38cda3964d50abf3efb626", + "version": 5, + "contents": { + "json": { + "id": "0x7cfc2f4e6743c28eee330e37ae5293166f2e9e397e38cda3964d50abf3efb626", + "count": "0" + } + }, + "dynamicFields": { + "nodes": [ + { + "value": { + "address": "0x09b23abe081f13c3e7e5ee8064d75b9b04144ab6b0a45a490afd1720f5c57c07", + "version": 6, + "contents": { + "json": { + "id": "0x09b23abe081f13c3e7e5ee8064d75b9b04144ab6b0a45a490afd1720f5c57c07", + "count": "0" + } + } + } + } + ] + } + } + } + ] + } + } + } +} + +task 12 'run-graphql'. lines 157-187: +Response: { + "data": { + "object": { + "dynamicFields": { + "nodes": [ + { + "value": { + "address": "0x7cfc2f4e6743c28eee330e37ae5293166f2e9e397e38cda3964d50abf3efb626", + "version": 7, + "contents": { + "json": { + "id": "0x7cfc2f4e6743c28eee330e37ae5293166f2e9e397e38cda3964d50abf3efb626", + "count": "1" + } + }, + "dynamicFields": { + "nodes": [ + { + "value": { + "address": "0x09b23abe081f13c3e7e5ee8064d75b9b04144ab6b0a45a490afd1720f5c57c07", + "version": 6, + "contents": { + "json": { + "id": "0x09b23abe081f13c3e7e5ee8064d75b9b04144ab6b0a45a490afd1720f5c57c07", + "count": "0" + } + } + } + } + ] + } + } + } + ] + } + } + } +} + +task 13 'run-graphql'. lines 189-219: +Response: { + "data": { + "object": { + "dynamicFields": { + "nodes": [ + { + "value": { + "address": "0x7cfc2f4e6743c28eee330e37ae5293166f2e9e397e38cda3964d50abf3efb626", + "version": 7, + "contents": { + "json": { + "id": "0x7cfc2f4e6743c28eee330e37ae5293166f2e9e397e38cda3964d50abf3efb626", + "count": "1" + } + }, + "dynamicFields": { + "nodes": [ + { + "value": { + "address": "0x09b23abe081f13c3e7e5ee8064d75b9b04144ab6b0a45a490afd1720f5c57c07", + "version": 8, + "contents": { + "json": { + "id": "0x09b23abe081f13c3e7e5ee8064d75b9b04144ab6b0a45a490afd1720f5c57c07", + "count": "1" + } + } + } + } + ] + } + } + } + ] + } + } + } +} + +task 14 'run-graphql'. lines 221-238: +Response: { + "data": { + "object": { + "dynamicFields": { + "nodes": [] + } + } + } +} + +task 15 'run-graphql'. lines 240-257: +Response: { + "data": { + "object": null + } +} + +task 16 'run-graphql'. lines 259-276: +Response: { + "data": { + "object": { + "dynamicFields": { + "nodes": [ + { + "value": { + "address": "0x09b23abe081f13c3e7e5ee8064d75b9b04144ab6b0a45a490afd1720f5c57c07", + "version": 6, + "contents": { + "json": { + "id": "0x09b23abe081f13c3e7e5ee8064d75b9b04144ab6b0a45a490afd1720f5c57c07", + "count": "0" + } + } + } + } + ] + } + } + } +} diff --git a/crates/sui-graphql-e2e-tests/tests/consistency/dynamic_fields/nested_dof.move b/crates/sui-graphql-e2e-tests/tests/consistency/dynamic_fields/nested_dof.move new file mode 100644 index 0000000000000..98216bc6e4083 --- /dev/null +++ b/crates/sui-graphql-e2e-tests/tests/consistency/dynamic_fields/nested_dof.move @@ -0,0 +1,276 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// 1. create Parent1 (2) +// 2. create Child1 (3) +// 3. create Child2 (4) +// 4. add Child1 to Parent1 -> Parent1 (5), Child1 (5) +// 5. add Child2 as a nested child object by borrowing Parent1.Child1 -> Parent1 (6), Child1 (5), Child2 (6) +// 6. mutate child1 -> Parent1 (7), Child1 (7), Child2 (6) +// 7. mutate child2 through parent -> Parent1 (8), Child1 (7), Child2 (8) + +// dynamic fields rooted on parent +// Parent(5) -> Child1 (5) -> None // add child1 as child to parent1 +// Parent(6) -> Child1 (5) -> Child2 (6) // add child2 as a child to child1 by borrowing child1 from parent +// Parent(7) -> Child1 (7) -> Child2 (6) // mutate child1 +// Parent(8) -> Child1 (7) -> Child2 (8) // mutate nested child2 by borrowing from child1 + +// query with Child1 as the root: +// Child1 (5) -> None +// Child1 (7) -> Child2 (6) + +//# init --protocol-version 44 --addresses Test=0x0 --accounts A --simulator + +//# publish +module Test::M1 { + use sui::dynamic_object_field as ofield; + + public struct Parent has key, store { + id: UID, + count: u64 + } + + public struct Child has key, store { + id: UID, + count: u64, + } + + public entry fun parent(recipient: address, ctx: &mut TxContext) { + transfer::public_transfer( + Parent { id: object::new(ctx), count: 0 }, + recipient + ) + } + + public entry fun mutate_parent(parent: &mut Parent) { + parent.count = parent.count + 42; + } + + public entry fun child(recipient: address, ctx: &mut TxContext) { + transfer::public_transfer( + Child { id: object::new(ctx), count: 0 }, + recipient + ) + } + + public fun add_child(parent: &mut Parent, child: Child, name: u64) { + ofield::add(&mut parent.id, name, child); + } + + public fun add_nested_child(parent: &mut Parent, child_name: u64, nested_child: Child, nested_child_name: u64) { + let child: &mut Child = ofield::borrow_mut(&mut parent.id, child_name); + ofield::add(&mut child.id, nested_child_name, nested_child); + } + + public fun mutate_child_on_parent(parent: &mut Parent, child_name: u64) { + let child: &mut Child = ofield::borrow_mut(&mut parent.id, child_name); + child.count = child.count + 1; + } + + public fun mutate_nested_child_on_parent(parent: &mut Parent, child_name: u64, nested_child_name: u64) { + let child: &mut Child = ofield::borrow_mut(&mut parent.id, child_name); + let nested_child: &mut Child = ofield::borrow_mut(&mut child.id, nested_child_name); + nested_child.count = nested_child.count + 1; + } +} + +//# run Test::M1::parent --sender A --args @A + +//# run Test::M1::child --sender A --args @A + +//# run Test::M1::child --sender A --args @A + +//# run Test::M1::add_child --sender A --args object(2,0) object(3,0) 42 + +//# run Test::M1::add_nested_child --sender A --args object(2,0) 42 object(4,0) 420 + +//# run Test::M1::mutate_child_on_parent --sender A --args object(2,0) 42 + +//# run Test::M1::mutate_nested_child_on_parent --sender A --args object(2,0) 42 420 + +//# create-checkpoint + +//# run-graphql +{ + object(address: "@{obj_2_0}", version: 5) { + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + } + } + } + } + } + } + } + } + } +} + +//# run-graphql +{ + object(address: "@{obj_2_0}", version: 6) { + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + } + } + } + } + } + } + } + } + } +} + +//# run-graphql +{ + object(address: "@{obj_2_0}", version: 7) { + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + } + } + } + } + } + } + } + } + } +} + +//# run-graphql +{ + object(address: "@{obj_2_0}", version: 8) { + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + } + } + } + } + } + } + } + } + } +} + +//# run-graphql +{ + object(address: "@{obj_3_0}", version: 5) { + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + } + } + } + } + } +} + +//# run-graphql +{ + object(address: "@{obj_3_0}", version: 6) { + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + } + } + } + } + } +} + +//# run-graphql +{ + object(address: "@{obj_3_0}", version: 7) { + dynamicFields { + nodes { + value { + ... on MoveObject { + address + version + contents { + json + } + } + } + } + } + } +} diff --git a/crates/sui-graphql-rpc/schema/current_progress_schema.graphql b/crates/sui-graphql-rpc/schema/current_progress_schema.graphql index cb7aa0e80470f..277462a920808 100644 --- a/crates/sui-graphql-rpc/schema/current_progress_schema.graphql +++ b/crates/sui-graphql-rpc/schema/current_progress_schema.graphql @@ -881,9 +881,10 @@ type DynamicField { """ name: MoveValue """ - The actual data stored in the dynamic field. - The returned dynamic field is an object if its return type is MoveObject, - in which case it is also accessible off-chain via its address. + The returned dynamic field is an object if its return type is `MoveObject`, + in which case it is also accessible off-chain via its address. Its contents + will be from the latest version that is at most equal to its parent object's + version """ value: DynamicFieldValue } diff --git a/crates/sui-graphql-rpc/src/types/coin.rs b/crates/sui-graphql-rpc/src/types/coin.rs index fd75a0d459b7f..eb9dbd967ce23 100644 --- a/crates/sui-graphql-rpc/src/types/coin.rs +++ b/crates/sui-graphql-rpc/src/types/coin.rs @@ -244,7 +244,7 @@ impl Coin { name: DynamicFieldName, ) -> Result> { OwnerImpl::from(&self.super_.super_) - .dynamic_field(ctx, name, Some(self.super_.super_.version_impl())) + .dynamic_field(ctx, name, Some(self.super_.root_version())) .await } @@ -261,7 +261,7 @@ impl Coin { name: DynamicFieldName, ) -> Result> { OwnerImpl::from(&self.super_.super_) - .dynamic_object_field(ctx, name, Some(self.super_.super_.version_impl())) + .dynamic_object_field(ctx, name, Some(self.super_.root_version())) .await } @@ -284,7 +284,7 @@ impl Coin { after, last, before, - Some(self.super_.super_.version_impl()), + Some(self.super_.root_version()), ) .await } @@ -336,7 +336,8 @@ impl Coin { // To maintain consistency, the returned cursor should have the same upper-bound as the // checkpoint found on the cursor. let cursor = stored.cursor(checkpoint_viewed_at).encode_cursor(); - let object = Object::try_from_stored_history_object(stored, checkpoint_viewed_at)?; + let object = + Object::try_from_stored_history_object(stored, checkpoint_viewed_at, None)?; let move_ = MoveObject::try_from(&object).map_err(|_| { Error::Internal(format!( diff --git a/crates/sui-graphql-rpc/src/types/coin_metadata.rs b/crates/sui-graphql-rpc/src/types/coin_metadata.rs index f64c3c29c2a70..19b0524a4b14c 100644 --- a/crates/sui-graphql-rpc/src/types/coin_metadata.rs +++ b/crates/sui-graphql-rpc/src/types/coin_metadata.rs @@ -233,7 +233,7 @@ impl CoinMetadata { name: DynamicFieldName, ) -> Result> { OwnerImpl::from(&self.super_.super_) - .dynamic_field(ctx, name, Some(self.super_.super_.version_impl())) + .dynamic_field(ctx, name, Some(self.super_.root_version())) .await } @@ -250,7 +250,7 @@ impl CoinMetadata { name: DynamicFieldName, ) -> Result> { OwnerImpl::from(&self.super_.super_) - .dynamic_object_field(ctx, name, Some(self.super_.super_.version_impl())) + .dynamic_object_field(ctx, name, Some(self.super_.root_version())) .await } @@ -273,7 +273,7 @@ impl CoinMetadata { after, last, before, - Some(self.super_.super_.version_impl()), + Some(self.super_.root_version()), ) .await } diff --git a/crates/sui-graphql-rpc/src/types/dynamic_field.rs b/crates/sui-graphql-rpc/src/types/dynamic_field.rs index 9be777d2fcf36..94e1c1cea0017 100644 --- a/crates/sui-graphql-rpc/src/types/dynamic_field.rs +++ b/crates/sui-graphql-rpc/src/types/dynamic_field.rs @@ -98,9 +98,10 @@ impl DynamicField { Ok(Some(MoveValue::new(type_tag, Base64::from(bcs)))) } - /// The actual data stored in the dynamic field. - /// The returned dynamic field is an object if its return type is MoveObject, - /// in which case it is also accessible off-chain via its address. + /// The returned dynamic field is an object if its return type is `MoveObject`, + /// in which case it is also accessible off-chain via its address. Its contents + /// will be from the latest version that is at most equal to its parent object's + /// version async fn value(&self, ctx: &Context<'_>) -> Result> { if self.df_kind == DynamicFieldType::DynamicObject { // If `df_kind` is a DynamicObject, the object we are currently on is the field object, @@ -111,12 +112,7 @@ impl DynamicField { let obj = MoveObject::query( ctx, self.df_object_id, - Object::under_parent( - // TODO (RPC-131): The dynamic object field value's version should be bounded by - // the field's parent version, not the version of the field object itself. - self.super_.super_.version_impl(), - self.super_.super_.checkpoint_viewed_at, - ), + Object::under_parent(self.root_version(), self.super_.super_.checkpoint_viewed_at), ) .await .extend()?; @@ -228,7 +224,7 @@ impl DynamicField { let history_object = StoredHistoryObject::from(stored_obj); let gql_object = - Object::try_from_stored_history_object(history_object, checkpoint_viewed_at)?; + Object::try_from_stored_history_object(history_object, checkpoint_viewed_at, None)?; let super_ = match MoveObject::try_from(&gql_object) { Ok(object) => Some(object), @@ -285,7 +281,11 @@ impl DynamicField { // checkpoint found on the cursor. let cursor = stored.cursor(checkpoint_viewed_at).encode_cursor(); - let object = Object::try_from_stored_history_object(stored, checkpoint_viewed_at)?; + let object = Object::try_from_stored_history_object( + stored, + checkpoint_viewed_at, + parent_version, + )?; let move_ = MoveObject::try_from(&object).map_err(|_| { Error::Internal(format!( @@ -300,6 +300,10 @@ impl DynamicField { Ok(conn) } + + pub(crate) fn root_version(&self) -> u64 { + self.super_.root_version() + } } impl TryFrom for DynamicField { diff --git a/crates/sui-graphql-rpc/src/types/move_object.rs b/crates/sui-graphql-rpc/src/types/move_object.rs index faa67dd7d3238..a4128c7235745 100644 --- a/crates/sui-graphql-rpc/src/types/move_object.rs +++ b/crates/sui-graphql-rpc/src/types/move_object.rs @@ -312,7 +312,7 @@ impl MoveObject { name: DynamicFieldName, ) -> Result> { OwnerImpl::from(&self.super_) - .dynamic_field(ctx, name, Some(self.super_.version_impl())) + .dynamic_field(ctx, name, Some(self.root_version())) .await } @@ -329,7 +329,7 @@ impl MoveObject { name: DynamicFieldName, ) -> Result> { OwnerImpl::from(&self.super_) - .dynamic_object_field(ctx, name, Some(self.super_.version_impl())) + .dynamic_object_field(ctx, name, Some(self.root_version())) .await } @@ -346,14 +346,7 @@ impl MoveObject { before: Option, ) -> Result> { OwnerImpl::from(&self.super_) - .dynamic_fields( - ctx, - first, - after, - last, - before, - Some(self.super_.version_impl()), - ) + .dynamic_fields(ctx, first, after, last, before, Some(self.root_version())) .await } @@ -461,6 +454,13 @@ impl MoveObject { }) .await } + + /// Root parent object version for dynamic fields. + /// + /// Check [`Object::root_version`] for details. + pub(crate) fn root_version(&self) -> u64 { + self.super_.root_version() + } } impl TryFrom<&Object> for MoveObject { diff --git a/crates/sui-graphql-rpc/src/types/object.rs b/crates/sui-graphql-rpc/src/types/object.rs index f1be5cb3de29a..6814da6304fd1 100644 --- a/crates/sui-graphql-rpc/src/types/object.rs +++ b/crates/sui-graphql-rpc/src/types/object.rs @@ -53,6 +53,18 @@ pub(crate) struct Object { pub kind: ObjectKind, /// The checkpoint sequence number at which this was viewed at. pub checkpoint_viewed_at: u64, + /// Root parent object version for dynamic fields. + /// + /// This enables consistent dynamic field reads in the case of chained dynamic object fields, + /// e.g., `Parent -> DOF1 -> DOF2`. In such cases, the object versions may end up like + /// `Parent >= DOF1, DOF2` but `DOF1 < DOF2`. Thus, database queries for dynamic fields must + /// bound the object versions by the version of the root object of the tree. + /// + /// Essentially, lamport timestamps of objects are updated for all top-level mutable objects + /// provided as inputs to a transaction as well as any mutated dynamic child objects. However, + /// any dynamic child objects that were loaded but not actually mutated don't end up having + /// their versions updated. + root_version: u64, } /// Type to implement GraphQL fields that are shared by all Objects. @@ -462,7 +474,7 @@ impl Object { name: DynamicFieldName, ) -> Result> { OwnerImpl::from(self) - .dynamic_field(ctx, name, Some(self.version_impl())) + .dynamic_field(ctx, name, Some(self.root_version())) .await } @@ -479,7 +491,7 @@ impl Object { name: DynamicFieldName, ) -> Result> { OwnerImpl::from(self) - .dynamic_object_field(ctx, name, Some(self.version_impl())) + .dynamic_object_field(ctx, name, Some(self.root_version())) .await } @@ -496,7 +508,7 @@ impl Object { before: Option, ) -> Result> { OwnerImpl::from(self) - .dynamic_fields(ctx, first, after, last, before, Some(self.version_impl())) + .dynamic_fields(ctx, first, after, last, before, Some(self.root_version())) .await } @@ -672,15 +684,24 @@ impl Object { /// `checkpoint_viewed_at` represents the checkpoint sequence number at which this `Object` was /// constructed in. This is stored on `Object` so that when viewing that entity's state, it will /// be as if it was read at the same checkpoint. + /// + /// `root_version` represents the version of the root object in some nested chain of dynamic + /// fields. This should typically be left `None`, unless the object(s) being resolved is a + /// dynamic field, or if `root_version` has been explicitly set for this object. If None, then + /// we use [`version_for_dynamic_fields`] to infer a root version to then propagate from this + /// object down to its dynamic fields. pub(crate) fn from_native( address: SuiAddress, native: NativeObject, checkpoint_viewed_at: u64, + root_version: Option, ) -> Object { + let root_version = root_version.unwrap_or_else(|| version_for_dynamic_fields(&native)); Object { address, kind: ObjectKind::NotIndexed(native), checkpoint_viewed_at, + root_version, } } @@ -702,6 +723,13 @@ impl Object { } } + /// Root parent object version for dynamic fields. + /// + /// Check [`Object::root_version`] for details. + pub(crate) fn root_version(&self) -> u64 { + self.root_version + } + /// Query the database for a `page` of objects, optionally `filter`-ed. /// /// `checkpoint_viewed_at` represents the checkpoint sequence number at which this page was @@ -766,7 +794,8 @@ impl Object { // To maintain consistency, the returned cursor should have the same upper-bound as the // checkpoint found on the cursor. let cursor = stored.cursor(checkpoint_viewed_at).encode_cursor(); - let object = Object::try_from_stored_history_object(stored, checkpoint_viewed_at)?; + let object = + Object::try_from_stored_history_object(stored, checkpoint_viewed_at, None)?; conn.edges.push(Edge::new(cursor, downcast(object)?)); } @@ -854,9 +883,16 @@ impl Object { /// `checkpoint_viewed_at` represents the checkpoint sequence number at which this `Object` was /// constructed in. This is stored on `Object` so that when viewing that entity's state, it will /// be as if it was read at the same checkpoint. + /// + /// `root_version` represents the version of the root object in some nested chain of dynamic + /// fields. This should typically be left `None`, unless the object(s) being resolved is a + /// dynamic field, or if `root_version` has been explicitly set for this object. If None, then + /// we use [`version_for_dynamic_fields`] to infer a root version to then propagate from this + /// object down to its dynamic fields. pub(crate) fn try_from_stored_history_object( history_object: StoredHistoryObject, checkpoint_viewed_at: u64, + root_version: Option, ) -> Result { let address = addr(&history_object.object_id)?; @@ -881,10 +917,13 @@ impl Object { Error::Internal(format!("Failed to deserialize object {address}")) })?; + let root_version = + root_version.unwrap_or_else(|| version_for_dynamic_fields(&native_object)); Ok(Self { address, kind: ObjectKind::Indexed(native_object, history_object), checkpoint_viewed_at, + root_version, }) } NativeObjectStatus::WrappedOrDeleted => Ok(Self { @@ -896,11 +935,23 @@ impl Object { checkpoint_sequence_number: history_object.checkpoint_sequence_number, }), checkpoint_viewed_at, + root_version: history_object.object_version as u64, }), } } } +/// We're deliberately choosing to use a child object's version as the root here, and letting the +/// caller override it with the actual root object's version if it has access to it. +/// +/// Using the child object's version as the root means that we're seeing the dynamic field tree +/// under this object at the state resulting from the transaction that produced this version. +/// +/// See [`Object::root_version`] for more details on parent/child object version mechanics. +fn version_for_dynamic_fields(native: &NativeObject) -> u64 { + native.as_inner().version().into() +} + impl ObjectFilter { /// Try to create a filter whose results are the intersection of objects in `self`'s results and /// objects in `other`'s results. This may not be possible if the resulting filter is @@ -1167,8 +1218,12 @@ impl Loader for Db { continue; } - let object = - Object::try_from_stored_history_object(stored.clone(), key.checkpoint_viewed_at)?; + let object = Object::try_from_stored_history_object( + stored.clone(), + key.checkpoint_viewed_at, + // This conversion will use the object's own version as the `Object::root_version`. + None, + )?; result.insert(*key, object); } @@ -1255,8 +1310,13 @@ impl Loader for Db { for (group_key, stored) in group.map_err(|e| Error::Internal(format!("Failed to fetch objects: {e}")))? { - let object = - Object::try_from_stored_history_object(stored, group_key.checkpoint_viewed_at)?; + let object = Object::try_from_stored_history_object( + stored, + group_key.checkpoint_viewed_at, + // If `LatestAtKey::parent_version` is set, it must have been correctly + // propagated from the `Object::root_version` of some object. + group_key.parent_version, + )?; let key = LatestAtKey { id: object.address, diff --git a/crates/sui-graphql-rpc/src/types/stake.rs b/crates/sui-graphql-rpc/src/types/stake.rs index 7ba77e7e87790..43e7b4126e4d8 100644 --- a/crates/sui-graphql-rpc/src/types/stake.rs +++ b/crates/sui-graphql-rpc/src/types/stake.rs @@ -252,7 +252,7 @@ impl StakedSui { name: DynamicFieldName, ) -> Result> { OwnerImpl::from(&self.super_.super_) - .dynamic_field(ctx, name, Some(self.super_.super_.version_impl())) + .dynamic_field(ctx, name, Some(self.super_.root_version())) .await } @@ -269,7 +269,7 @@ impl StakedSui { name: DynamicFieldName, ) -> Result> { OwnerImpl::from(&self.super_.super_) - .dynamic_object_field(ctx, name, Some(self.super_.super_.version_impl())) + .dynamic_object_field(ctx, name, Some(self.super_.root_version())) .await } @@ -292,7 +292,7 @@ impl StakedSui { after, last, before, - Some(self.super_.super_.version_impl()), + Some(self.super_.root_version()), ) .await } diff --git a/crates/sui-graphql-rpc/src/types/suins_registration.rs b/crates/sui-graphql-rpc/src/types/suins_registration.rs index 145a29ea7d093..9e708314bc70b 100644 --- a/crates/sui-graphql-rpc/src/types/suins_registration.rs +++ b/crates/sui-graphql-rpc/src/types/suins_registration.rs @@ -289,7 +289,7 @@ impl SuinsRegistration { name: DynamicFieldName, ) -> Result> { OwnerImpl::from(&self.super_.super_) - .dynamic_field(ctx, name, Some(self.super_.super_.version_impl())) + .dynamic_field(ctx, name, Some(self.super_.root_version())) .await } @@ -306,7 +306,7 @@ impl SuinsRegistration { name: DynamicFieldName, ) -> Result> { OwnerImpl::from(&self.super_.super_) - .dynamic_object_field(ctx, name, Some(self.super_.super_.version_impl())) + .dynamic_object_field(ctx, name, Some(self.super_.root_version())) .await } @@ -329,7 +329,7 @@ impl SuinsRegistration { after, last, before, - Some(self.super_.super_.version_impl()), + Some(self.super_.root_version()), ) .await } @@ -509,7 +509,8 @@ impl NameService { // name_record. We then assign it to the correct field on `domain_expiration` based on the // address. for result in results { - let object = Object::try_from_stored_history_object(result, checkpoint_viewed_at)?; + let object = + Object::try_from_stored_history_object(result, checkpoint_viewed_at, None)?; let move_object = MoveObject::try_from(&object).map_err(|_| { Error::Internal(format!( "Expected {0} to be a NameRecord, but it's not a Move Object.", diff --git a/crates/sui-graphql-rpc/src/types/transaction_block_kind/end_of_epoch.rs b/crates/sui-graphql-rpc/src/types/transaction_block_kind/end_of_epoch.rs index a03c8a9f26ef0..e9e8ed5e84964 100644 --- a/crates/sui-graphql-rpc/src/types/transaction_block_kind/end_of_epoch.rs +++ b/crates/sui-graphql-rpc/src/types/transaction_block_kind/end_of_epoch.rs @@ -203,7 +203,7 @@ impl ChangeEpochTransaction { ); let runtime_id = native.id(); - let object = Object::from_native(SuiAddress::from(runtime_id), native, c.c); + let object = Object::from_native(SuiAddress::from(runtime_id), native, c.c, None); let package = MovePackage::try_from(&object) .map_err(|_| Error::Internal("Failed to create system package".to_string())) .extend()?; diff --git a/crates/sui-graphql-rpc/src/types/transaction_block_kind/genesis.rs b/crates/sui-graphql-rpc/src/types/transaction_block_kind/genesis.rs index 0bcda76dbba8d..97d6646c50486 100644 --- a/crates/sui-graphql-rpc/src/types/transaction_block_kind/genesis.rs +++ b/crates/sui-graphql-rpc/src/types/transaction_block_kind/genesis.rs @@ -58,7 +58,7 @@ impl GenesisTransaction { let native = NativeObject::new_from_genesis(data, owner, TransactionDigest::genesis_marker()); - let object = Object::from_native(SuiAddress::from(native.id()), native, c.c); + let object = Object::from_native(SuiAddress::from(native.id()), native, c.c, None); connection.edges.push(Edge::new(c.encode_cursor(), object)); } diff --git a/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap b/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap index 93b5cf533a75a..7e02b01da8968 100644 --- a/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap +++ b/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap @@ -885,9 +885,10 @@ type DynamicField { """ name: MoveValue """ - The actual data stored in the dynamic field. - The returned dynamic field is an object if its return type is MoveObject, - in which case it is also accessible off-chain via its address. + The returned dynamic field is an object if its return type is `MoveObject`, + in which case it is also accessible off-chain via its address. Its contents + will be from the latest version that is at most equal to its parent object's + version """ value: DynamicFieldValue } @@ -4198,4 +4199,3 @@ schema { query: Query mutation: Mutation } -