Lattice Workshop - Step 7

Integrate with Persona

So far we've been using the player's wallet address in the OwnedBy component to represent ownership. Now let's integrate with Persona instead.

All we need to do is to switch OwnedBy from a AddressComponent to a UintComponent and replace most occurences of msg.sender with getPersonaId() in the contracts and all occurences of signer.address with personaId on the client.

That's all! With this tiny change our game is now fully integrated with Persona.

Files changed (6) hide show
  1. client/src/Game.ts +2 -4
  2. client/src/react/App.tsx +3 -6
  3. client/src/systems/InputSystem.ts +3 -3
  4. contracts/deploy/002_deploy_game.ts +1 -1
  5. contracts/lattice.config.ts +1 -1
  6. contracts/src/Game.sol +15 -7
client/src/Game.ts CHANGED
@@ -10,13 +10,11 @@ import {
10
10
  import { createMapping, loadEvents, setupContracts, setupMappings } from "../packages/lattice-eth-middleware";
11
11
  import { setupPhaser } from "@latticexyz/phaser-middleware";
12
12
  import {
13
- createAddressComponent,
14
13
  createBoolComponent,
15
14
  createCoordComponent,
16
15
  createStringComponent,
17
16
  createTupleComponent,
18
17
  createUintComponent,
19
- decodeAddressComponent,
20
18
  decodeBoolComponent,
21
19
  decodeCoordComponent,
22
20
  decodeStringComponent,
@@ -56,7 +54,7 @@ export async function createGame(contractAddress: string, privateKey: string, ch
56
54
  const Position = createCoordComponent(world, "Position");
57
55
  const Texture = createStringComponent(world, "Texture");
58
56
  const Appearance = createUintComponent(world, "Appearance");
59
- const OwnedBy = createAddressComponent(world, "OwnedBy");
57
+ const OwnedBy = createUintComponent(world, "OwnedBy");
60
58
  const Movable = createBoolComponent(world, "Movable");
61
59
  const Miner = createBoolComponent(world, "Miner");
62
60
  const Mined = createBoolComponent(world, "Mined");
@@ -88,7 +86,7 @@ export async function createGame(contractAddress: string, privateKey: string, ch
88
86
  ...createMapping(componentAddresses.position, Position, decodeCoordComponent),
89
87
  ...createMapping(componentAddresses.texture, Texture, decodeStringComponent),
90
88
  ...createMapping(componentAddresses.appearance, Appearance, decodeUintComponent),
91
- ...createMapping(componentAddresses.ownedBy, OwnedBy, decodeAddressComponent),
89
+ ...createMapping(componentAddresses.ownedBy, OwnedBy, decodeUintComponent),
92
90
  ...createMapping(componentAddresses.movable, Movable, decodeBoolComponent),
93
91
  ...createMapping(componentAddresses.miner, Miner, decodeBoolComponent),
94
92
  ...createMapping(componentAddresses.mined, Mined, decodeBoolComponent),
client/src/react/App.tsx CHANGED
@@ -14,17 +14,14 @@ export const App: React.FC<{ context: Context }> = observer(({ context }) => {
14
14
  world,
15
15
  components: { OwnedBy, Heart },
16
16
  } = context;
17
- const lostEntityQuery = defineExitQuery(world, [HasValue(OwnedBy, { value: context.signer.address })]);
18
- const ownedByQuery = defineQuery([HasValue(OwnedBy, { value: context.signer.address })]);
17
+ const lostEntityQuery = defineExitQuery(world, [HasValue(OwnedBy, { value: context.personaId })]);
18
+ const ownedByQuery = defineQuery([HasValue(OwnedBy, { value: context.personaId })]);
19
19
  return reaction(
20
20
  () => lostEntityQuery.get(),
21
21
  (lostEntities) => {
22
22
  if (lostEntities.size == 0) return;
23
23
  // If the player doesn't own a heart anymore, the game is lost
24
- if (
25
- ownedByQuery.get().size > 0 &&
26
- !exists([Has(Heart), HasValue(OwnedBy, { value: context.signer.address })])
27
- ) {
24
+ if (ownedByQuery.get().size > 0 && !exists([Has(Heart), HasValue(OwnedBy, { value: context.personaId })])) {
28
25
  setModalText("Game over");
29
26
  } else {
30
27
  // Flash a quick modal if the player lost an entity
client/src/systems/InputSystem.ts CHANGED
@@ -8,7 +8,7 @@ export function createInputSystem(context: Context) {
8
8
  components: { OwnedBy, Selected, Position },
9
9
  phaser: { input, map: tilemap },
10
10
  api: { spawn, actionDirection },
11
- signer,
11
+ personaId,
12
12
  } = context;
13
13
 
14
14
  input.onKeyPress(
@@ -38,7 +38,7 @@ export function createInputSystem(context: Context) {
38
38
  filter((coord) => coord.x >= 0 && coord.y >= 0 && coord.x < tilemap.width && coord.y < tilemap.height) // Filter clicks outside the map
39
39
  )
40
40
  .subscribe((coord) => {
41
- if (exists([HasValue(OwnedBy, { value: signer.address })]) == undefined) {
41
+ if (exists([HasValue(OwnedBy, { value: personaId })]) == undefined) {
42
42
  // If not spawned, spawn
43
43
  console.log("Spawning at", coord);
44
44
  spawn(coord);
@@ -50,7 +50,7 @@ export function createInputSystem(context: Context) {
50
50
  if (selectedEntity != undefined) removeComponent(Selected, selectedEntity);
51
51
 
52
52
  // Add the Selected component to the entity below the cursor
53
- const entityAtPos = exists([HasValue(Position, coord), HasValue(OwnedBy, { value: signer.address })]);
53
+ const entityAtPos = exists([HasValue(Position, coord), HasValue(OwnedBy, { value: personaId })]);
54
54
  if (entityAtPos) {
55
55
  console.log("Selected entity", entityAtPos, "at", coord);
56
56
  setComponent(Selected, entityAtPos, {});
contracts/deploy/002_deploy_game.ts CHANGED
@@ -33,7 +33,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
33
33
  from: deployer,
34
34
  log: true,
35
35
  autoMine: true,
36
- args: [world.address], // Add personaMirrorAddress here when integrating persona
36
+ args: [world.address, personaMirrorAddress],
37
37
  });
38
38
 
39
39
  const gameContract = await hre.ethers.getContract('Game', deployer);
contracts/lattice.config.ts CHANGED
@@ -2,7 +2,7 @@ export const componentConfig = {
2
2
  position: 'CoordComponent',
3
3
  texture: 'StringComponent',
4
4
  appearance: 'UintComponent',
5
- ownedBy: 'AddressComponent',
5
+ ownedBy: 'UintComponent',
6
6
  movable: 'BoolComponent',
7
7
  miner: 'BoolComponent',
8
8
  mined: 'BoolComponent',
contracts/src/Game.sol CHANGED
@@ -8,16 +8,16 @@ import { QueryFragment, QueryType, LibQuery } from 'lattice-ecs/LibQuery.sol';
8
8
  import { CoordComponent, Coord } from './components/CoordComponent.sol';
9
9
  import { UintComponent } from './components/UintComponent.sol';
10
10
  import { StringComponent } from './components/StringComponent.sol';
11
- import { AddressComponent } from './components/AddressComponent.sol';
12
11
  import { BoolComponent } from './components/BoolComponent.sol';
13
12
  import { TupleComponent } from './components/TupleComponent.sol';
14
13
  import { manhattan } from './utils.sol';
14
+ import { PersonaMirror } from 'persona/L2/PersonaMirror.sol';
15
15
 
16
16
  struct Components {
17
17
  CoordComponent position;
18
18
  StringComponent texture;
19
19
  UintComponent appearance;
20
- AddressComponent ownedBy;
20
+ UintComponent ownedBy;
21
21
  BoolComponent movable;
22
22
  BoolComponent miner;
23
23
  BoolComponent mined;
@@ -33,6 +33,7 @@ enum Texture {
33
33
  }
34
34
 
35
35
  contract Game {
36
+ PersonaMirror public personaMirror;
36
37
  address public world;
37
38
  address public owner;
38
39
  uint256 public width = 32;
@@ -45,7 +46,7 @@ contract Game {
45
46
  _;
46
47
  }
47
48
  modifier onlyEntityOwner(uint256 entity) {
48
- require(c.ownedBy.getValue(entity) == msg.sender, 'invalid owner');
49
+ require(c.ownedBy.getValue(entity) == getPersona(), 'invalid owner');
49
50
  _;
50
51
  }
51
52
 
@@ -59,9 +60,10 @@ contract Game {
59
60
  _;
60
61
  }
61
62
 
62
- constructor(address _world) {
63
+ constructor(address _world, address _personaMirror) {
63
64
  owner = msg.sender;
64
65
  world = _world;
66
+ personaMirror = PersonaMirror(_personaMirror);
65
67
  }
66
68
 
67
69
  function registerComponents(Components memory _components, address[] memory _componentList) public onlyContractOwner {
@@ -82,6 +84,12 @@ contract Game {
82
84
  c.texture.set(uint256(Texture.Ground), ground);
83
85
  }
84
86
 
87
+ function getPersona() internal view returns (uint256) {
88
+ uint256 personaId = personaMirror.getActivePersona(msg.sender, address(this));
89
+ require(personaMirror.isAuthorized(personaId, msg.sender, address(this), msg.sig), 'persona not authorized');
90
+ return personaId;
91
+ }
92
+
85
93
  /**
86
94
  * Remove all components from the given entity
87
95
  */
@@ -125,11 +133,11 @@ contract Game {
125
133
 
126
134
  function spawn(Coord memory center) public inBounds(center) {
127
135
  // Check player is not spawned yet
128
- require(c.ownedBy.getEntitiesWithValue(msg.sender).length == 0, 'already spawned');
136
+ require(c.ownedBy.getEntitiesWithValue(getPersona()).length == 0, 'already spawned');
129
137
 
130
138
  // Create player entity (to indicate spawn)
131
139
  uint256 playerEntity = World(world).getNumEntities();
132
- c.ownedBy.set(playerEntity, msg.sender);
140
+ c.ownedBy.set(playerEntity, getPersona());
133
141
 
134
142
  // Check spawn area is empty, then mine
135
143
  for (uint256 dx; dx < 3; dx++) {
@@ -141,7 +149,7 @@ contract Game {
141
149
  // Spawn entities
142
150
  uint256 entity = World(world).getNumEntities();
143
151
  c.position.set(entity, coord);
144
- c.ownedBy.set(entity, msg.sender);
152
+ c.ownedBy.set(entity, getPersona());
145
153
 
146
154
  // Put heart at the center
147
155
  if (dx == 1 && dy == 1) {