Skip to content

Commit f3dd8f5

Browse files
authored
Allow offchain datasource creation in offchain handlers (#4713)
* core: allow creation of offchain datasources in offchain handlers * tests: Add integration tests for offchain datasource creation in offchain handlers * tests/runner-tests : Test failure of spawning onchan ds from offchain handler
1 parent 6f907fc commit f3dd8f5

File tree

6 files changed

+219
-54
lines changed

6 files changed

+219
-54
lines changed

core/src/subgraph/runner.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ where
433433
// Check for offchain events and process them, including their entity modifications in the
434434
// set to be transacted.
435435
let offchain_events = self.ctx.offchain_monitor.ready_offchain_events()?;
436-
let (offchain_mods, processed_data_sources) = self
436+
let (offchain_mods, processed_data_sources, persisted_off_chain_data_sources) = self
437437
.handle_offchain_triggers(offchain_events, &block)
438438
.await?;
439439
mods.extend(offchain_mods);
@@ -476,12 +476,13 @@ where
476476

477477
let BlockState {
478478
deterministic_errors,
479-
persisted_data_sources,
479+
mut persisted_data_sources,
480480
..
481481
} = block_state;
482482

483483
let first_error = deterministic_errors.first().cloned();
484484

485+
persisted_data_sources.extend(persisted_off_chain_data_sources);
485486
store
486487
.transact_block_operations(
487488
block_ptr,
@@ -703,9 +704,17 @@ where
703704
&mut self,
704705
triggers: Vec<offchain::TriggerData>,
705706
block: &Arc<C::Block>,
706-
) -> Result<(Vec<EntityModification>, Vec<StoredDynamicDataSource>), Error> {
707+
) -> Result<
708+
(
709+
Vec<EntityModification>,
710+
Vec<StoredDynamicDataSource>,
711+
Vec<StoredDynamicDataSource>,
712+
),
713+
Error,
714+
> {
707715
let mut mods = vec![];
708716
let mut processed_data_sources = vec![];
717+
let mut persisted_data_sources = vec![];
709718

710719
for trigger in triggers {
711720
// Using an `EmptyStore` and clearing the cache for each trigger is a makeshift way to
@@ -742,10 +751,17 @@ where
742751
})?;
743752

744753
anyhow::ensure!(
745-
!block_state.has_created_data_sources(),
746-
"Attempted to create data source in offchain data source handler. This is not yet supported.",
754+
!block_state.has_created_on_chain_data_sources(),
755+
"Attempted to create on-chain data source in offchain data source handler. This is not yet supported.",
747756
);
748757

758+
let (data_sources, _) =
759+
self.create_dynamic_data_sources(block_state.drain_created_data_sources())?;
760+
761+
// Add entity operations for the new data sources to the block state
762+
// and add runtimes for the data sources to the subgraph instance.
763+
self.persist_dynamic_data_sources(&mut block_state, data_sources);
764+
749765
// This propagates any deterministic error as a non-deterministic one. Which might make
750766
// sense considering offchain data sources are non-deterministic.
751767
if let Some(err) = block_state.deterministic_errors.into_iter().next() {
@@ -759,9 +775,10 @@ where
759775
.modifications,
760776
);
761777
processed_data_sources.extend(block_state.processed_data_sources);
778+
persisted_data_sources.extend(block_state.persisted_data_sources)
762779
}
763780

764-
Ok((mods, processed_data_sources))
781+
Ok((mods, processed_data_sources, persisted_data_sources))
765782
}
766783
}
767784

tests/runner-tests/file-data-sources/schema.graphql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,10 @@ type IpfsFile @entity {
66
type IpfsFile1 @entity {
77
id: ID!
88
content: String!
9+
}
10+
11+
type SpawnTestEntity @entity {
12+
id: ID!
13+
content: String!
14+
context: String!
915
}
Lines changed: 75 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
1-
import { ethereum, dataSource, BigInt, Bytes, DataSourceContext } from '@graphprotocol/graph-ts'
2-
import { TestEvent } from '../generated/Contract/Contract'
3-
import { IpfsFile, IpfsFile1 } from '../generated/schema'
1+
import {
2+
ethereum,
3+
dataSource,
4+
BigInt,
5+
Bytes,
6+
DataSourceContext,
7+
} from "@graphprotocol/graph-ts";
8+
import { TestEvent } from "../generated/Contract/Contract";
9+
import { IpfsFile, IpfsFile1, SpawnTestEntity } from "../generated/schema";
410

511
// CID of `file-data-sources/abis/Contract.abi` after being processed by graph-cli.
6-
const KNOWN_CID = "QmQ2REmceVtzawp7yrnxLQXgNNCtFHEnig6fL9aqE1kcWq"
12+
const KNOWN_CID = "QmQ2REmceVtzawp7yrnxLQXgNNCtFHEnig6fL9aqE1kcWq";
713

814
export function handleBlock(block: ethereum.Block): void {
9-
let entity = new IpfsFile("onchain")
10-
entity.content = "onchain"
11-
entity.save()
15+
let entity = new IpfsFile("onchain");
16+
entity.content = "onchain";
17+
entity.save();
1218

1319
// This will create the same data source twice, once at block 0 and another at block 2.
1420
// The creation at block 2 should be detected as a duplicate and therefore a noop.
1521
if (block.number == BigInt.fromI32(0) || block.number == BigInt.fromI32(2)) {
16-
dataSource.create("File", [KNOWN_CID])
22+
dataSource.create("File", [KNOWN_CID]);
1723
}
1824

1925
if (block.number == BigInt.fromI32(1)) {
20-
let entity = IpfsFile.load("onchain")!
21-
assert(entity.content == "onchain")
26+
let entity = IpfsFile.load("onchain")!;
27+
assert(entity.content == "onchain");
2228

2329
// The test assumes file data sources are processed in the block in which they are created.
2430
// So the ds created at block 0 will have been processed.
@@ -27,17 +33,16 @@ export function handleBlock(block: ethereum.Block): void {
2733
assert(IpfsFile.load(KNOWN_CID) == null);
2834

2935
// Test that using an invalid CID will be ignored
30-
dataSource.create("File", ["hi, I'm not valid"])
36+
dataSource.create("File", ["hi, I'm not valid"]);
3137
}
3238

33-
34-
// This will invoke File1 data source with same CID, which will be used
39+
// This will invoke File1 data source with same CID, which will be used
3540
// to test whether same cid is triggered across different data source.
3641
if (block.number == BigInt.fromI32(3)) {
3742
// Test that onchain data sources cannot read offchain data (again, but this time more likely to hit the DB than the write queue).
3843
assert(IpfsFile.load(KNOWN_CID) == null);
3944

40-
dataSource.create("File1", [KNOWN_CID])
45+
dataSource.create("File1", [KNOWN_CID]);
4146
}
4247
}
4348

@@ -46,17 +51,32 @@ export function handleTestEvent(event: TestEvent): void {
4651

4752
if (command == "createFile2") {
4853
// Will fail the subgraph when processed due to mismatch in the entity type and 'entities'.
49-
dataSource.create("File2", [KNOWN_CID])
54+
dataSource.create("File2", [KNOWN_CID]);
5055
} else if (command == "saveConflictingEntity") {
5156
// Will fail the subgraph because the same entity has been created in a file data source.
52-
let entity = new IpfsFile(KNOWN_CID)
53-
entity.content = "empty"
54-
entity.save()
57+
let entity = new IpfsFile(KNOWN_CID);
58+
entity.content = "empty";
59+
entity.save();
5560
} else if (command == "createFile1") {
5661
// Will fail the subgraph with a conflict between two entities created by offchain data sources.
5762
let context = new DataSourceContext();
5863
context.setBytes("hash", event.block.hash);
59-
dataSource.createWithContext("File1", [KNOWN_CID], context)
64+
dataSource.createWithContext("File1", [KNOWN_CID], context);
65+
} else if (command == "spawnOffChainHandlerTest") {
66+
// Used to test the spawning of a file data source from another file data source handler.
67+
// `SpawnTestHandler` will spawn a file data source that will be handled by `spawnOffChainHandlerTest`,
68+
// which creates another file data source `OffChainDataSource`, which will be handled by `handleSpawnedTest`.
69+
let context = new DataSourceContext();
70+
context.setString("command", command);
71+
dataSource.createWithContext("SpawnTestHandler", [KNOWN_CID], context);
72+
} else if (command == "spawnOnChainHandlerTest") {
73+
// Used to test the failure of spawning of on-chain data source from a file data source handler.
74+
// `SpawnTestHandler` will spawn a file data source that will be handled by `spawnTestHandler`,
75+
// which creates an `OnChainDataSource`, which should fail since spawning onchain datasources
76+
// from offchain handlers is not allowed.
77+
let context = new DataSourceContext();
78+
context.setString("command", command);
79+
dataSource.createWithContext("SpawnTestHandler", [KNOWN_CID], context);
6080
} else {
6181
assert(false, "Unknown command: " + command);
6282
}
@@ -66,22 +86,48 @@ export function handleFile(data: Bytes): void {
6686
// Test that offchain data sources cannot read onchain data.
6787
assert(IpfsFile.load("onchain") == null);
6888

69-
if (dataSource.stringParam() != "QmVkvoPGi9jvvuxsHDVJDgzPEzagBaWSZRYoRDzU244HjZ") {
89+
if (
90+
dataSource.stringParam() != "QmVkvoPGi9jvvuxsHDVJDgzPEzagBaWSZRYoRDzU244HjZ"
91+
) {
7092
// Test that an offchain data source cannot read from another offchain data source.
71-
assert(IpfsFile.load("QmVkvoPGi9jvvuxsHDVJDgzPEzagBaWSZRYoRDzU244HjZ") == null);
93+
assert(
94+
IpfsFile.load("QmVkvoPGi9jvvuxsHDVJDgzPEzagBaWSZRYoRDzU244HjZ") == null
95+
);
7296
}
7397

74-
let entity = new IpfsFile(dataSource.stringParam())
75-
entity.content = data.toString()
76-
entity.save()
98+
let entity = new IpfsFile(dataSource.stringParam());
99+
entity.content = data.toString();
100+
entity.save();
77101

78102
// Test that an offchain data source can load its own entities
79-
let loaded_entity = IpfsFile.load(dataSource.stringParam())!
80-
assert(loaded_entity.content == entity.content)
103+
let loaded_entity = IpfsFile.load(dataSource.stringParam())!;
104+
assert(loaded_entity.content == entity.content);
81105
}
82106

83107
export function handleFile1(data: Bytes): void {
84-
let entity = new IpfsFile1(dataSource.stringParam())
85-
entity.content = data.toString()
86-
entity.save()
108+
let entity = new IpfsFile1(dataSource.stringParam());
109+
entity.content = data.toString();
110+
entity.save();
111+
}
112+
113+
// Used to test spawning a file data source from another file data source handler.
114+
// This function spawns a file data source that will be handled by `handleSpawnedTest`.
115+
export function spawnTestHandler(data: Bytes): void {
116+
let context = new DataSourceContext();
117+
context.setString("file", "fromSpawnTestHandler");
118+
let command = dataSource.context().getString("command");
119+
if (command == "spawnOffChainHandlerTest") {
120+
dataSource.createWithContext("OffChainDataSource", [KNOWN_CID], context);
121+
} else if (command == "spawnOnChainHandlerTest") {
122+
dataSource.createWithContext("OnChainDataSource", [KNOWN_CID], context);
123+
}
124+
}
125+
126+
// This is the handler for the data source spawned by `spawnOffChainHandlerTest`.
127+
export function handleSpawnedTest(data: Bytes): void {
128+
let entity = new SpawnTestEntity(dataSource.stringParam());
129+
let context = dataSource.context().getString("file");
130+
entity.content = data.toString();
131+
entity.context = context;
132+
entity.save();
87133
}

tests/runner-tests/file-data-sources/subgraph.yaml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@ dataSources:
2424
handler: handleTestEvent
2525
file: ./src/mapping.ts
2626
templates:
27+
- kind: ethereum/contract
28+
name: OnChainDataSource
29+
network: test
30+
source:
31+
abi: Contract
32+
mapping:
33+
kind: ethereum/events
34+
apiVersion: 0.0.7
35+
language: wasm/assemblyscript
36+
entities:
37+
- Gravatar
38+
abis:
39+
- name: Contract
40+
file: ./abis/Contract.abi
41+
blockHandlers:
42+
- handler: handleBlock
43+
eventHandlers:
44+
- event: TestEvent(string)
45+
handler: handleTestEvent
46+
file: ./src/mapping.ts
2747
- kind: file/ipfs
2848
name: File
2949
mapping:
@@ -62,4 +82,30 @@ templates:
6282
- name: Contract
6383
file: ./abis/Contract.abi
6484
handler: handleFile1
85+
file: ./src/mapping.ts
86+
- kind: file/ipfs
87+
name: SpawnTestHandler
88+
mapping:
89+
kind: ethereum/events
90+
apiVersion: 0.0.7
91+
language: wasm/assemblyscript
92+
entities:
93+
- SpawnTestEntity
94+
abis:
95+
- name: Contract
96+
file: ./abis/Contract.abi
97+
handler: spawnTestHandler
98+
file: ./src/mapping.ts
99+
- kind: file/ipfs
100+
name: OffChainDataSource
101+
mapping:
102+
kind: ethereum/events
103+
apiVersion: 0.0.7
104+
language: wasm/assemblyscript
105+
entities:
106+
- SpawnTestEntity
107+
abis:
108+
- name: Contract
109+
file: ./abis/Contract.abi
110+
handler: handleSpawnedTest
65111
file: ./src/mapping.ts

tests/src/fixture/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ pub async fn wait_for_sync(
496496
continue;
497497
}
498498
};
499-
499+
info!(logger, "TEST: sync status: {:?}", block_ptr);
500500
let status = store.status_for_id(deployment.id);
501501

502502
if let Some(fatal_error) = status.fatal_error {

0 commit comments

Comments
 (0)