Skip to main content

Bulk import

The bulk import page (/records/import, Bulk import on the dashboard and in the Issue menu) issues many credentials from one export. It runs entirely in the issuer's browser with the issuer's wallet, and every credential goes through the same CredentialService.issue path as the single issue form. The files never leave the computer. The file formats are in Bulk import files; the handbook guide is docs/import/README.md.

Preconditions​

  • v0.3, an active issuer on a plain wallet, the system not paused.
  • Pinata and session settings on the server, when any row has a document.
  • A browser that keeps localStorage (progress is kept there) and, ideally, supports the Web Locks API (to stop two tabs running the same import).

The stepper​

The page header reads Bulk import, Import credentials: "Issue a whole cohort from one export. Every row is checked before your wallet is asked for anything." A four-step bar shows Choose file, Check, Issue, Results.

1. Choose your files​

Section Choose your files ("Export the cohort from your student records system, then choose the file here."):

FieldPurpose
Export formatcsv, json or template (the adapter)
Records file (CSV or JSON)"One row per credential. Nothing is signed until you start." Larger than 10 MB is refused.
Earlier result file (optional)"Carries on a run that was started in another browser."
Document folder (optional)"Holds the documents the records file names."

The page notes: "The files are read on this computer and are not uploaded. Each document is encrypted in your browser before it is stored." and "Your wallet asks for one signature for every credential, so keep the page open while it runs."

2. Check​

The adapter parses the file, then validateRecords checks every row against the issuer's record types (read from the chain) and the folder contents. The review section ("Checked in this browser", headed with the file name) shows either:

  • "N records, no problems." and "X with a document, Y data only. Your wallet will ask for N signatures and one more to protect the documents, and N transactions will be sent.", or
  • "N problems to fix in the file. Nothing was signed." with each problem as Row 12, studentId: ... and "Correct these in your spreadsheet or records system, export the file again and choose it here."

Holder details are never shown in the review. Warnings (unused columns, a document shared by several rows, documents not checked) are listed but do not block. When an earlier result file was given: "N rows from the earlier result file will be recognised once the chain confirms them for your account." Changed rows (an external id issued before with different details) are listed: "N rows changed since they were issued. They will be issued as new credentials, and the earlier ones stay valid until revoked or replaced."

Options​

Section Before you start ("Both are off unless you tick them."):

OptionEffect
Replace the earlier credential of each changed row"Marks the earlier one as superseded in the same transaction." (issueReplacing). On a set without atomic replace: "Not available: this contract set cannot replace in one transaction. Revoke the earlier ones from the dashboard."
Add a selective disclosure signature to each credential"One more wallet prompt for every credential." A refused or failed signature never undoes an issued credential and is noted in the result.

3. Issue​

Start issuing begins the run (disabled with "Fix the file above and choose it again to start." while there are problems). Section Issuing: "Confirm each request in your wallet and keep this page open until the run ends." The latest rows appear with their outcome. Stop after this row asks the run to stop once the current row is done ("The run stops once the current row is finished.").

4. Results​

Section The run is complete or The run stopped, with counts per outcome and a table Record, Outcome, Credential or reason (each credential id links to its verify page).

Outcome badgeStateMeaning
IssuedissuedIssued in this run; its package is in the archive
Already doneskippedIssued in an earlier run and confirmed on chain
Found on chainrecoveredAn earlier try that the progress record had as pending is on chain after all
UnconfirmedunconfirmedAn earlier try may still land; not sent again automatically
FailedfailedWith the reason
Not runnot-runThe run stopped before this row

Buttons: Download packages (N) (credential-packages.zip), Download result file (import-result.csv), Issue the unconfirmed rows again when there are any, and Finish. Finish stays locked until the archive is downloaded ("Finish unlocks once the packages are downloaded."), and the browser asks to confirm before leaving the page while packages are unsaved. Notice Keep the archive private: each package holds the key that opens its holder's document, so send each holder only their own; the packages "cannot be rebuilt from the chain".

One row, step by step​

The executor is a PinnedIssuerExecutor wrapping a SingleIssueExecutor (src/services/import/). The pinned wrapper refuses every row once the wallet account differs from the one the run began with ("The wallet account changed during the import. Switch back to the issuing account and start the run again."). The single executor uploads the row's document (or anchors the SHA-256 of zero bytes for a data-only row), calls CredentialService.issue, and adds the disclosure signature only when the option is on.

Why a run is repeat-safe​

Rows are keyed by what they would issue (name, id, type, programme, award, date and document name), hashed, never by the external id. The progress record is kept in localStorage under certichain.import.<key>, where the key is a hash of the issuer address and chain id. It holds only hashed row keys, credential ids, states and transaction nonces.

runImport (src/lib/import/run.ts) applies these rules for each row:

SituationWhat happens
Stored as issued, and the chain holds that credentialAlready done (skipped); nothing is sent
Stored as pending, and the chain holds one of the recorded credential idsFound on chain (recovered); the row is marked issued
Stored as pending, not on chain, and the account's confirmed nonce has passed every recorded tryNone can land any more, so the row is issued again automatically
Stored as pending, not on chain, a try may still landUnconfirmed, with "An earlier try may have been sent and is not on the chain yet. Wait a few minutes and run again, or choose to issue it again." or, when the transaction is stuck, "An earlier try is still waiting in the network. Wait for it, or speed it up or cancel it in the wallet, then run again."
Not storedIssued

The credential id of every try (and, once sent, its transaction nonce) is recorded before the run waits for the transaction. A run that dies in between is therefore recognised from the chain and never issues a row twice by itself. Issue the unconfirmed rows again overrides the wait; the page warns: "Issuing the unconfirmed rows again is safe only if their first try never lands: a late first try could then make a second credential."

Carrying on elsewhere​

Choosing the same records file again in the same browser carries the run on. In another browser, or after site data was cleared, the issuer adds the earlier result file as Earlier result file. Its rows are read as hints only: every credential id is still confirmed on the chain for this issuer before a row is skipped, so an edited or copied result file cannot make a row look done.

When a run stops by itself​

StopMessage
A rejected wallet request"The request was rejected in your wallet. The import stopped; start it again to continue."
Wrong network, a contract wallet, or a changed accountThe specific error
NotIssuer, SystemPaused or InvalidSignatureThe matching issuing message; every later row would fail the same way
Three failed rows in a row"The run stopped after three failures in a row. Fix the cause, then start it again with the same file."
Stop after this row"The run was stopped. Start it again with the same file to carry on."
Damaged progress record"The saved progress of this import is damaged, so the run cannot tell which rows were issued. Nothing was sent."
Another tab runs the same import"This import is already running in another tab or window. Finish or close that one first."

Wallet prompts​

WhenKindCount
First document rowSignature × 2 (EncryptionKey), plus a sign-in message if neededOnce per run
Each rowSignature (Credential)One per row
Each rowTransaction (issue or issueReplacing)One per row
Each row, option tickedSignature (disclosure attestation)One per row

No student-salt signature is asked for, so imported credentials use a fresh subject salt and are not delivered to linked student wallets. Holders receive their package from the archive.

Checking a file without a wallet​

npm run import:check -- records.csv
npm run import:check -- export.txt --adapter template --folder ./documents --types "Short Course, Transcript Addendum"

It runs the same parsing and validation, prints counts, warnings and every problem with its row and field (never holder details), and exits 0 (clean), 1 (problems) or 2 (wrong arguments or unreadable file).

Limits​

  • One wallet prompt sequence per row; a large cohort on one account takes a long time. Batch anchoring is planned behind the same IssueExecutor interface.
  • Without the result file, choosing the same file in another browser or after clearing site data would issue the rows again. Keep the result file with the packages.
  • A try that was never sent (the wallet prompt was closed) stays unconfirmed until the issuer chooses to issue it again.
  • 5,000 rows and 10 MB per file.

Code map​

ConcernFile
Pagesrc/app/records/import/ImportPage.tsx, ImportWorkspace.tsx, ImportSteps.tsx, ChooseFiles.tsx, ImportReview.tsx, ImportOptions.tsx, ImportProgress.tsx, ImportResults.tsx, OutcomeBadge.tsx, folder-files.ts, use-import.ts
Reading and checkingsrc/lib/import/adapters.ts, csv-adapter.ts, json-adapter.ts, csv.ts, validate.ts, prepare.ts, changed.ts
Plan and runsrc/lib/import/plan.ts, run.ts, store.ts, lock.ts, errors.ts, progress.ts
Outputssrc/lib/import/outputs.ts, zip.ts
Executorssrc/services/import/single-issue-executor.ts, pinned-issuer-executor.ts
Command linesrc/lib/import/check-cli.ts, scripts/import-check.ts

Tests​

  • src/lib/import/run.test.ts, store.test.ts, lock.test.ts, errors.test.ts, plan.test.ts, validate.test.ts, prepare.test.ts, changed.test.ts, progress.test.ts, outputs.test.ts, adapters.test.ts, csv.test.ts, check-cli.test.ts
  • src/services/import/single-issue-executor.test.ts, pinned-issuer-executor.test.ts
  • src/app/records/import/ImportWorkspace.test.tsx, ImportResults.test.tsx, use-import.test.tsx, folder-files.test.ts
  • E2E: e2e/journeys/bulk-import.journey.ts (a run that is stopped and resumed)