Skip to content
Snippets Groups Projects
Select Git revision
  • main default protected
  • gtest-custom
  • gtest
  • account-balance
  • hugo-rebase-mr-23
  • fix-23-before-rebase
  • add-certifications-date
  • proxy-limiter
  • convert-hash-bytes-to-hexa
  • 0.2.9
  • 0.2.8
  • 0.2.7
  • 0.2.6
  • 0.2.4
  • 0.2.3
  • 0.2.2
  • 0.2.0
  • 0.1.3
  • 0.1.2
  • 0.1.1
20 results

data_handler.ts

Blame
  • data_handler.ts 26.50 KiB
    import { strict as assert } from 'assert';
    import { In } from "typeorm";
    import {
      Account,
      Cert,
      CertEvent,
      ChangeOwnerKey,
      Event,
      EventType,
      Identity,
      IdentityStatus,
      MembershipEvent,
      SmithEvent,
      SmithEventType,
      Smith,
      SmithCert,
      SmithStatus,
      PopulationHistory,
      Transfer,
      TxComment,
      UdReeval,
      UniversalDividend,
      Validator,
    } from "./model";
    import { Address, Ctx, Data, IdtyIndex, NewData } from "./types_custom";
    import { hexToString } from "./utils";
    import { events } from "./types";
    import { getCommentType } from "./comment";
    
    export class DataHandler {
      private data: Data;
    
      constructor() {
        this.data = {
          accounts: new Map(),
          identities: new Map(),
          smiths: new Map(),
          populationHistories: [],
          validators: new Map(),
          membershipEvents: [],
          smithEvents: [],
          changeOwnerKey: [],
          transfers: new Map(),
          certification: new Map(),
          certEvent: [],
          smithCert: new Map(),
          universalDividend: [],
          udReeval: [],
          comments: []
        };
      }
    
      async processNewData(newData: NewData, ctx: Ctx) {
    
        // Process population history by adding the value
        // of the last point in database.
        if (newData.populationHistories) {
          const lastHistory = await ctx.store.findOneOrFail(PopulationHistory, {
            where: {},
            order: { blockNumber: 'DESC' }
          });
    
          this.data.populationHistories = newData.populationHistories.map(history => (new PopulationHistory({
            activeAccountCount: history.activeAccountCount + lastHistory.activeAccountCount,
            memberCount: history.memberCount + lastHistory.memberCount,
            smithCount: history.smithCount + lastHistory.smithCount,
            blockNumber: history.blockNumber,
            id: `population-${history.blockNumber}`,
          })));
        }
    
        // Process accounts
        for (const accountevt of newData.accounts) {
          const newAccount = new Account({ id: accountevt.address, createdOn: accountevt.blockNumber, isActive: true });
          this.data.accounts.set(accountevt.address, newAccount);
          ctx.log.info(`Added account ${accountevt}`);
        }
    
        // Process killed accounts
        for (const accountId of newData.killedAccounts) {
          const killedAccount = await this.getAccountByAddressOrFail(ctx, accountId);
          killedAccount.isActive = false;
          this.data.accounts.set(accountId, killedAccount);
          ctx.log.info(`Killed account ${accountId}`);
        }
    
        // Process transfers
        for (const transfer of newData.transfers) {
          //   const fromAccount = await this.getOrCreateAccount(ctx, transfer.from);
          //   const toAccount = await this.getOrCreateAccount(ctx, transfer.to);
          ctx.log.info(
            `New transaction: ${transfer.from} to ${transfer.to} of ${transfer.amount} tokens`
          );
          // should never fail because source of transfer must be an existing account
          const fromAccount = await this.getAccountByAddressOrFail(ctx, transfer.from);
          // shoud never fail because destination of transfer must be existing account or raise System.NewAccount
          const toAccount = await this.getAccountByAddressOrFail(ctx, transfer.to);
          const newTransfer = new Transfer({
            id: transfer.id,
            blockNumber: transfer.blockNumber,
            timestamp: transfer.timestamp,
            from: fromAccount,
            to: toAccount,
            amount: transfer.amount,
          });
          this.data.transfers.set(transfer.id, newTransfer);
        }
    
        // Process comments
        for (const commentEvt of newData.comments) {
          const sender = await this.getAccountByAddressOrFail(ctx, commentEvt.sender);
          // create new comment anyway and add it
          const remarkHex: string = commentEvt.event.call!.args["remark"] // extract raw data as hex string (temporary before better in Duniter)
          const remarkRaw = Buffer.from(remarkHex.substring(2), "hex") // convert it back to bytes
          const [commentType, remarkString] = getCommentType(remarkRaw)
          const txcomment = new TxComment({
            id: commentEvt.event.id,
            author: sender,
            remarkBytes: remarkRaw,
            remark: remarkString,
            hash: commentEvt.hash, // raw hex hash (temporary)
            type: commentType,
            event: await ctx.store.getOrFail(Event, commentEvt.event!.id),
            blockNumber: commentEvt.blockNumber,
          })
          this.data.comments.push(txcomment)
    
          // browse all events in the same extrinsic (batch) 
          for (const evt of commentEvt.event.extrinsic!.events) {
            // only allow commenting transfers
            if (evt.name == events.balances.transfer.name) {
              const transfer = this.data.transfers.get(evt.id)! // transfers have been handled just before
              transfer.comment = txcomment
            }
          }
        }
    
        // Process identities created
        for (const identity of newData.identitiesCreated) {
          const account = await this.getAccountByAddressOrFail(ctx, identity.accountId);
          const newIdentity = new Identity({
            id: identity.event.id,
            index: identity.index,
            // Using the id of the creation event as the name for unconfirmed identities
            // dirty hack to allow name to be non nullable
            name: identity.event.id,
            status: IdentityStatus.Unconfirmed,
            account,
            isMember: false,
            lastChangeOn: identity.blockNumber,
            createdOn: identity.blockNumber,
            createdIn: await ctx.store.getOrFail(Event, identity.event.id),
            expireOn: identity.expireOn,
          });
          this.data.identities.set(identity.index, newIdentity);
        }
    
        // Process identities confirmed
        for (const identity of newData.identitiesConfirmed) {
          const idty = await this.getIdtyByIndexOrFail(ctx, identity.index);
          idty.name = hexToString(identity.name);
          idty.status = IdentityStatus.Unvalidated;
          idty.lastChangeOn = identity.blockNumber;
          idty.expireOn = identity.expireOn;
          this.data.identities.set(identity.index, idty);
        }
    
        // // note: no need for identity validation event since handled in membershipAdded
        // // Process identities validated
        // for (const identity of newData.identitiesValidated) {
        //   const idty = await this.getIdtyByIndexOrFail(ctx, identity.index);
        //   idty.status = IdentityStatus.Member;
        //   idty.lastChangeOn = identity.blockNumber;
        //   idty.expireOn = identity.expireOn;
        //   this.data.identities.set(identity.index, idty);
        // }
    
        // Process identities removed
        for (const evt of newData.identitiesRemoved) {
          const { index, reason } = evt;
          const idty = await this.getIdtyWithAccountByIndexOrFail(ctx, index);
          ctx.log.info(`Set identity ${index} status to Removed for reason ${reason.__kind}`);
          // the only point of keeping removed identities is to block use of their account
          // (and maybe pseudo later on)
          idty.status = IdentityStatus.Removed;
          idty.accountRemoved = idty.account!; // we are sure that a non-removed identity has non-null account
          idty.account = null; // having nullable field allows to keep the @unique constraint
          this.data.identities.set(index, idty);
        }
    
        // Process identities revoked
        for (const evt of newData.identitiesRevoked) {
          const { index, reason, blockNumber, expireOn } = evt;
          const idty = await this.getIdtyByIndexOrFail(ctx, index);
          ctx.log.info(`Set identity ${index} status to Revoked for reason ${reason.__kind}`);
          idty.status = IdentityStatus.Revoked;
          idty.expireOn = expireOn;
          // only touch if manual revocation
          if (reason.__kind == "User") {
            idty.lastChangeOn = blockNumber;
          }
          this.data.identities.set(index, idty);
        }
    
        // Process identity owner key changes
        for (const idtyChange of newData.idtyChangedOwnerKey) {
          const idty = await this.getIdtyWithAccountByIndexOrFail(ctx, idtyChange.index);
          const new_account = await this.getOrCreateAccount(ctx, idtyChange.accountId);
          // add change owner key event
          this.data.changeOwnerKey.push(
            new ChangeOwnerKey({
              id: idtyChange.id,
              identity: idty,
              previous: idty.account!, // changing owner key is only possible for a non-removed identity
              next: new_account,
              blockNumber: idtyChange.blockNumber,
            })
          );
          // after adding the event we can update the identity
          idty.account = new_account;
          idty.lastChangeOn = idtyChange.blockNumber;
          this.data.identities.set(idtyChange.index, idty);
    
          // Related to https://git.duniter.org/nodes/rust/duniter-v2s/-/issues/245.
          // An account can be dissociated from an identity even if the validator is online.
          // To circumvent this problem, we keep a mapping between the account and the last
          // associated identity.
          const smith = this.data.smiths.get(idtyChange.index) ??
            await ctx.store.findOneBy(Smith, { index: idtyChange.index });
          if (smith && smith.smithStatus == SmithStatus.Smith) {
            // if an active smith changed owner key, we track this with a new validator
            const validator = new Validator({ id: idtyChange.accountId, index: idtyChange.index })
            this.data.validators.set(idtyChange.accountId, validator);
          }
        }
    
        // Process membership added
        for (const evt of newData.membershipAdded) {
          const { index, expireOn, event } = evt;
    
          const identity = await this.getIdtyByIndexOrFail(ctx, index);
          identity.status = IdentityStatus.Member
          identity.expireOn = expireOn;
          identity.isMember = true;
          identity.lastChangeOn = event.block.height;
    
          // Create membership
          this.data.membershipEvents.push(
            new MembershipEvent({
              id: `membership-creation-${index}-${event.id}`,
              identity,
              eventType: EventType.Creation,
              event: await ctx.store.getOrFail(Event, event.id),
              blockNumber: event.block.height,
            })
          );
          this.data.identities.set(identity.index, identity);
        }
    
        // Process membership renewed
        for (const evt of newData.membershipRenewed) {
          const { index, expireOn, event } = evt;
          const identity = await this.getIdtyByIndexOrFail(ctx, index);
    
          identity.status = IdentityStatus.Member
          identity.expireOn = expireOn;
          identity.isMember = true;
          identity.lastChangeOn = event.block.height;
    
          // Create membership
          this.data.membershipEvents.push(
            new MembershipEvent({
              id: `membership-renewal-${index}-${event.id}`,
              identity,
              eventType: EventType.Renewal,
              event: await ctx.store.getOrFail(Event, event.id),
              blockNumber: event.block.height,
            })
          );
          this.data.identities.set(identity.index, identity);
        }
    
        // Process membership removed
        for (const evt of newData.membershipRemoved) {
          const { index, event, expireOn } = evt;
          const identity = await this.getIdtyByIndexOrFail(ctx, index);
          identity.status = IdentityStatus.NotMember;
          identity.isMember = false;
          // identity.lastChangeOn = event.block.height; // no: only if active revocation
          identity.expireOn = expireOn
    
          this.data.membershipEvents.push(
            new MembershipEvent({
              id: `membership-removal-${index}-${event.id}`,
              identity,
              eventType: EventType.Removal,
              event: await ctx.store.getOrFail(Event, event.id),
              blockNumber: event.block.height,
            })
          );
          this.data.identities.set(identity.index, identity);
        }
    
        // Process certifications creations
        for (const c of newData.certCreation) {
          const { issuerId, receiverId, blockNumber, expireOn, event } = c;
          // first creation of the cert
          let cert = await ctx.store.findOne(Cert, {
            relations: { issuer: true, receiver: true },
            where: { issuer: { index: issuerId }, receiver: { index: receiverId } },
          });
    
          const eventObj = await ctx.store.getOrFail(Event, event.id);
          if (cert == null) {
            const issuer = await this.getIdtyByIndexOrFail(ctx, issuerId);
            const receiver = await this.getIdtyByIndexOrFail(ctx, receiverId);
            cert = new Cert({
              id: event.id,
              isActive: true,
              issuer,
              receiver,
              createdOn: blockNumber,
              createdIn: eventObj,
              updatedOn: blockNumber,
              updatedIn: eventObj,
              expireOn
            });
            // the cert has already existed, expired, and is created again
            // we update it accordingly
          } else {
            cert.isActive = true;
            cert.updatedOn = blockNumber;
            cert.updatedIn = eventObj;
            cert.expireOn = expireOn;
          }
    
          // update cert and add event
          this.data.certification.set([issuerId, receiverId], cert);
          this.data.certEvent.push(
            new CertEvent({
              id: event.id,
              cert,
              eventType: EventType.Creation,
              event: eventObj,
              blockNumber,
            })
          );
        }
    
        // Process certifications renewals
        for (const c of newData.certRenewal) {
          const { issuerId, receiverId, blockNumber, expireOn, event } = c;
          // should never fail because cert renewal can only happen on existing cert
          // and can not be renewed at the same block as created (delay)
          const cert = await ctx.store.findOneOrFail(Cert, {
            relations: { issuer: true, receiver: true },
            where: { issuer: { index: issuerId }, receiver: { index: receiverId } },
          });
          const eventObj = await ctx.store.getOrFail(Event, event.id);
          // update expiration date
          cert.expireOn = expireOn;
          cert.updatedOn = blockNumber;
          cert.updatedIn = eventObj;
          this.data.certification.set([issuerId, receiverId], cert);
          this.data.certEvent.push(
            new CertEvent({
              id: event.id,
              cert,
              eventType: EventType.Renewal,
              event: eventObj,
              blockNumber,
            })
          );
        }
    
        // Process validators
        for (const { block, validatorId } of newData.validators) {
          // validator must already exist because it was created at the same time as smith, which is necessary to compute block
          const validator = await this.getValidatorByAddressOrFail(ctx, validatorId);
          // we are sure that a smith exist because they are created at the same time
          const smith = await this.getSmithByIndexOrFail(ctx, validator.index);
          smith.forged += 1;
          smith.lastForged = block
          this.data.smiths.set(smith.index, smith);
        }
    
        // Process certifications removals
        for (const c of newData.certRemoval) {
          const { issuerId, receiverId, blockNumber, event } = c;
          // should never fail because cert removal can only happen on existing cert
          // and cert should not be removed at their creation block
          const cert = await ctx.store.findOneOrFail(Cert, {
            relations: { issuer: true, receiver: true },
            where: { issuer: { index: issuerId }, receiver: { index: receiverId } },
          });
          // update cert
          cert.isActive = false;
          this.data.certification.set([issuerId, receiverId], cert);
          this.data.certEvent.push(
            new CertEvent({
              id: event.id,
              cert,
              eventType: EventType.Removal,
              event: await ctx.store.getOrFail(Event, event.id),
              blockNumber,
            })
          );
        }
    
        // Process Smith certifications
        for (const c of newData.smithCertAdded) {
          const { id, issuerId, receiverId, createdOn } = c;
          let cert = await ctx.store.findOne(SmithCert, {
            relations: { issuer: true, receiver: true },
            where: { issuer: { index: issuerId }, receiver: { index: receiverId } },
          });
          if (cert == null) {
            const issuer = await this.getSmithByIndexOrFail(ctx, issuerId);
            const receiver = await this.getSmithByIndexOrFail(ctx, receiverId);
            cert = new SmithCert({
              id,
              issuer,
              receiver,
              createdOn,
            });
          } else {
            cert.createdOn = createdOn;
          }
          this.data.smithCert.set([issuerId, receiverId], cert);
        }
    
        // Process remove smith cert
        for (const smithCertRemoved of newData.smithCertRemoved) {
          const { issuerId, receiverId } = smithCertRemoved;
          const smithCert = await ctx.store.findOneOrFail(SmithCert, {
            where: { issuer: { index: issuerId }, receiver: { index: receiverId } },
          });
          await ctx.store.remove(smithCert);
        }
    
        // Process Smith invitation sent
        for (const invitedSmith of newData.smithInvited) {
          const { idtyIndex, event } = invitedSmith;
    
          const smith = await this.getOrCreateSmith(ctx, idtyIndex);
          smith.smithStatus = SmithStatus.Invited;
          smith.lastChanged = event.block.height;
          this.data.smiths.set(idtyIndex, smith);
    
          this.data.smithEvents.push(
            new SmithEvent({
              id: `smith-invited-${idtyIndex}-${event.id}`,
              smith,
              eventType: SmithEventType.Invited,
              event: await ctx.store.getOrFail(Event, event.id),
              blockNumber: event.block.height,
            })
          );
        }
    
        // Process Smith invitation accepted
        for (const acceptedSmithInvitations of newData.smithAccepted) {
          const { idtyIndex, event } = acceptedSmithInvitations;
    
          const smith = await this.getSmithByIndexOrFail(ctx, idtyIndex);
          smith.smithStatus = SmithStatus.Pending;
          smith.lastChanged = event.block.height;
          this.data.smiths.set(idtyIndex, smith);
          this.data.smithEvents.push(
            new SmithEvent({
              id: `smith-accepted-${idtyIndex}-${event.id}`,
              smith,
              eventType: SmithEventType.Accepted,
              event: await ctx.store.getOrFail(Event, event.id),
              blockNumber: event.block.height,
            })
          );
        }
    
        // Process Smith promotion
        for (const promotedSmith of newData.smithPromoted) {
          const { idtyIndex, event } = promotedSmith;
    
          const smith = await this.getSmithByIndexOrFail(ctx, idtyIndex);
          smith.smithStatus = SmithStatus.Smith;
          smith.lastChanged = event.block.height;
          this.data.smiths.set(idtyIndex, smith);
          this.data.smithEvents.push(
            new SmithEvent({
              id: `smith-promoted-${idtyIndex}-${event.id}`,
              smith,
              eventType: SmithEventType.Promoted,
              event: await ctx.store.getOrFail(Event, event.id),
              blockNumber: event.block.height,
            })
          );
    
          const identity = await this.getIdtyWithAccountByIndexOrFail(ctx, idtyIndex);
          const smithAccount = identity.account!; // a smith can only be a non-removed identity
          const validator = new Validator({ id: smithAccount.id, index: idtyIndex })
          this.data.validators.set(smithAccount.id, validator);
        }
    
        // Process Smith exlusion
        for (const excludedSmith of newData.smithExcluded) {
          const { idtyIndex, event } = excludedSmith;
    
          const smith = await this.getSmithByIndexOrFail(ctx, idtyIndex);
          smith.smithStatus = SmithStatus.Excluded;
          smith.lastChanged = event.block.height;
          this.data.smiths.set(idtyIndex, smith);
          this.data.smithEvents.push(
            new SmithEvent({
              id: `smith-excluded-${idtyIndex}-${event.id}`,
              smith,
              eventType: SmithEventType.Excluded,
              event: await ctx.store.getOrFail(Event, event.id),
              blockNumber: event.block.height,
            })
          );
        }
    
        // Process account links
        for (const link of newData.accountLink) {
          // we can link an identity to a non-existing account
          const account = await this.getOrCreateAccount(ctx, link.accountId);
          // should never fail because identity must exist to be able to link itself
          const idty = await this.getIdtyByIndexOrFail(ctx, link.index);
          account.linkedIdentity = idty;
          this.data.accounts.set(account.id, account);
        }
    
        // Process account unlinks
        for (const unlink of newData.accountUnlink) {
          // should never fail because account must exist to unlink due to tx fees
          const account = await this.getAccountByAddressOrFail(ctx, unlink.accountId);
          account.linkedIdentity = null;
          this.data.accounts.set(account.id, account);
        }
    
        // Process universal dividend
        for (const ud of newData.universalDividend) {
          const { blockNumber, amount, monetaryMass, membersCount, event, timestamp } = ud;
          this.data.universalDividend.push(new UniversalDividend({
            id: event.id,
            timestamp: timestamp,
            amount: BigInt(amount),
            monetaryMass,
            membersCount: Number(membersCount),
            event: await ctx.store.getOrFail(Event, event.id),
            blockNumber,
          }));
        }
    
        // Process ud reeval
        for (const udReeval of newData.udReeval) {
          const { blockNumber, newUdAmount, monetaryMass, membersCount, event, timestamp } =
            udReeval;
          this.data.udReeval.push(new UdReeval({
            id: event.id,
            timestamp: timestamp,
            newUdAmount: BigInt(newUdAmount),
            monetaryMass,
            membersCount: Number(membersCount),
            event: await ctx.store.getOrFail(Event, event.id),
            blockNumber,
          }));
        }
      }
    
      // this is a hack to handle circular dependency between account and identity
      // account that do not exist in database are created first so that identities can be created after
      // then accounts can be modified and have linkedIdentity point to the newly created identity
      async handleNewAccountsApart(ctx: Ctx, newData: NewData) {
        // Combine account and account link sets to get unique candidates
        const newAccountCandidates = new Set<Address>([
          ...newData.accounts.map(a => a.address),
          ...newData.accountLink.map((link) => link.accountId),
        ]);
    
        // Return immediately if no new candidates are present
        if (newAccountCandidates.size === 0) {
          return;
        }
    
        // Retrieve existing account IDs from the database
        const existingAccounts = await ctx.store.findBy(Account, {
          id: In([...newAccountCandidates]),
        });
        const existingAccountIds = new Set(
          existingAccounts.map((account) => account.id)
        );
    
        // Filter and create accounts that don't already exist
        const accountsToCreate = newData.accounts
          .filter(a => !existingAccountIds.has(a.address))
          .map(a => new Account({ id: a.address, createdOn: a.blockNumber, isActive: true }));
    
        if (accountsToCreate.length > 0) {
          await ctx.store.insert(accountsToCreate);
          await ctx.store.commit();
        }
    
        // Update the Data structure with new accounts
        for (const account of accountsToCreate) {
          const existingData = await this.getAccountByAddressOrFail(ctx, account.id);
          if (existingData) {
            account.linkedIdentity = existingData.linkedIdentity;
          }
          this.data.accounts.set(account.id, account);
        }
      }
    
      // this is a hack to handle circular dependency between account and identity
      // identities that are created are handled apart from the ones who only changed
      async handleNewIdentitiesApart(ctx: Ctx, newData: NewData) {
        const identities: Array<Identity> = []; // collects identities (only newly created)
        for (const i of newData.identitiesCreated) {
          const idty = this.data.identities.get(i.index);
          assert(idty, "created identities must appear in prepared identities");
          this.data.identities.delete(i.index); // prevent from trying to add twice
          identities.push(idty);
        }
        if (identities.length === 0) return;
        // we are sure that all created identities actually do not already exist in database
        await ctx.store.insert(identities);
        await ctx.store.commit();
      }
    
      /// store prepared data into database
      async storeData(ctx: Ctx) {
        // UPSERT = update or insert if not existing
        // account can have already existed, been killed, and recreated
        await ctx.store.upsert([...this.data.accounts.values()]);
        // identities can have been changed (confirmed, change owner key...) or added (created)
        await ctx.store.upsert([...this.data.smiths.values()]);
        await ctx.store.upsert([...this.data.identities.values()]);
        await ctx.store.upsert([...this.data.validators.values()]);
        // membership can have been created, renewed, or removed
        await ctx.store.upsert([...this.data.membershipEvents.values()]);
        await ctx.store.upsert([...this.data.smithEvents.values()]);
        // certs can have been created, renewed, or removed
        await ctx.store.upsert([...this.data.certification.values()]);
        await ctx.store.upsert([...this.data.smithCert.values()]);
    
        // INSERT = these object can not exist before
        await ctx.store.insert(this.data.comments); // transfers depend on comments
        await ctx.store.insert([...this.data.transfers.values()]);
        await ctx.store.insert(this.data.changeOwnerKey);
        await ctx.store.insert(this.data.certEvent);
        await ctx.store.insert(this.data.universalDividend);
        await ctx.store.insert(this.data.udReeval);
        await ctx.store.insert(this.data.populationHistories);
    
        // Apply changes in database
        await ctx.store.commit();
      }
    
      async getOrCreateAccount(ctx: Ctx, id: Address): Promise<Account> {
        let account =
          this.data.accounts.get(id) ?? (await ctx.store.get(Account, id));
        if (account == null) {
          // we need to create it
          account = new Account({ id, isActive: true });
          this.data.accounts.set(id, account);
        }
        return account;
      }
    
      async getAccountByAddressOrFail(ctx: Ctx, address: Address): Promise<Account> {
        return (
          this.data.accounts.get(address) ??
          ctx.store.findOneByOrFail(Account, { id: address })
        );
      }
    
      async getIdtyByIndexOrFail(ctx: Ctx, index: IdtyIndex): Promise<Identity> {
        return (
          this.data.identities.get(index) ??
          ctx.store.findOneByOrFail(Identity, { index })
        );
      }
    
      async getSmithByIndexOrFail(ctx: Ctx, index: IdtyIndex): Promise<Smith> {
        return (
          this.data.smiths.get(index) ??
          ctx.store.findOneByOrFail(Smith, { index })
        );
      }
    
      async getIdtyWithAccountByIndexOrFail(ctx: Ctx, index: IdtyIndex): Promise<Identity> {
        return (
          this.data.identities.get(index) ??
          ctx.store.findOneOrFail(Identity, {
            relations: { account: true },
            where: { index: index },
          }))
      }
    
      async getValidatorByAddressOrFail(ctx: Ctx, address: Address): Promise<Validator> {
        return (
          this.data.validators.get(address) ??
          ctx.store.findOneByOrFail(Validator, { id: address })
        );
      }
    
      async getOrCreateSmith(ctx: Ctx, index: IdtyIndex): Promise<Smith> {
        const smith =
          this.data.smiths.get(index) ??
          await ctx.store.findOneBy(Smith, { index });
        if (smith) {
          return smith;
        }
        else {
          const identity = await this.getIdtyByIndexOrFail(ctx, index);
          return new Smith({
            id: `smith_${index}`,
            index: index,
            identity: identity,
            forged: 0,
          });
        }
      }
    }