diff --git a/Tzkt.Api/Controllers/ConstantsController.cs b/Tzkt.Api/Controllers/ConstantsController.cs
new file mode 100644
index 000000000..05348b252
--- /dev/null
+++ b/Tzkt.Api/Controllers/ConstantsController.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+
+using Tzkt.Api.Models;
+using Tzkt.Api.Repositories;
+using Tzkt.Api.Services.Cache;
+
+namespace Tzkt.Api.Controllers
+{
+ [ApiController]
+ [Route("v1/constants")]
+ public class ConstantsController : ControllerBase
+ {
+ private readonly ConstantsRepository Constants;
+ private readonly StateCache State;
+
+ public ConstantsController(ConstantsRepository constants, StateCache state)
+ {
+ Constants = constants;
+ State = state;
+ }
+
+ ///
+ /// Get global constants
+ ///
+ ///
+ /// Returns a list of global constants.
+ ///
+ /// Filters constants by global address (starts with `expr..`).
+ /// Filters constants by creation level.
+ /// Filters constants by creation time.
+ /// Filters constants by creator.
+ /// Filters constants by number of refs.
+ /// Filters constants by size in bytes.
+ /// Specify comma-separated list of fields to include into response or leave it undefined to return full object. If you select single field, response will be an array of values in both `.fields` and `.values` modes.
+ /// Sorts delegators by specified field. Supported fields: `id` (default), `creationLevel`, `size`, `refs`.
+ /// Specifies which or how many items should be skipped
+ /// Maximum number of items to return
+ ///
+ [HttpGet]
+ public async Task>> Get(
+ ExpressionParameter address,
+ Int32Parameter creationLevel,
+ TimestampParameter creationTime,
+ AccountParameter creator,
+ Int32Parameter refs,
+ Int32Parameter size,
+ SelectParameter select,
+ SortParameter sort,
+ OffsetParameter offset,
+ [Range(0, 10000)] int limit = 100)
+ {
+ #region validate
+ if (sort != null && !sort.Validate("creationLevel", "size", "refs"))
+ return new BadRequest($"{nameof(sort)}", "Sorting by the specified field is not allowed.");
+ #endregion
+
+ if (select == null)
+ return Ok(await Constants.Get(address, creationLevel, creationTime, creator, refs, size, sort, offset, limit));
+
+ if (select.Values != null)
+ {
+ if (select.Values.Length == 1)
+ return Ok(await Constants.Get(address, creationLevel, creationTime, creator, refs, size, sort, offset, limit, select.Values[0]));
+ else
+ return Ok(await Constants.Get(address, creationLevel, creationTime, creator, refs, size, sort, offset, limit, select.Values));
+ }
+ else
+ {
+ if (select.Fields.Length == 1)
+ return Ok(await Constants.Get(address, creationLevel, creationTime, creator, refs, size, sort, offset, limit, select.Fields[0]));
+ else
+ {
+ return Ok(new SelectionResponse
+ {
+ Cols = select.Fields,
+ Rows = await Constants.Get(address, creationLevel, creationTime, creator, refs, size, sort, offset, limit, select.Fields)
+ });
+ }
+ }
+ }
+
+ ///
+ /// Get global constant by address
+ ///
+ ///
+ /// Returns global constant with specified address (expression hash).
+ ///
+ /// Global address (starts with `expr..`)
+ ///
+ [HttpGet("{address}")]
+ public async Task GetByAddress(
+ [Required][ExpressionHash] string address)
+ {
+ var res = await Constants.Get(address, null, null, null, null, null, null, null, 1);
+ return res.FirstOrDefault();
+ }
+
+ ///
+ /// Get global constants count
+ ///
+ ///
+ /// Returns a number of global constants.
+ ///
+ /// Filters constants by number of refs.
+ ///
+ [HttpGet("count")]
+ public Task GetCount(Int32Parameter refs)
+ {
+ if (refs == null)
+ return Task.FromResult(State.Current.ConstantsCount);
+
+ return Constants.GetCount();
+ }
+ }
+}
diff --git a/Tzkt.Api/Models/Constant.cs b/Tzkt.Api/Models/Constant.cs
new file mode 100644
index 000000000..9a0be5f69
--- /dev/null
+++ b/Tzkt.Api/Models/Constant.cs
@@ -0,0 +1,48 @@
+using Netezos.Encoding;
+using System;
+
+namespace Tzkt.Api.Models
+{
+ public class Constant
+ {
+ ///
+ /// Global address (expression hash)
+ ///
+ public string Address { get; set; }
+
+ ///
+ /// Constant micheline expression
+ ///
+ public IMicheline Value { get; set; }
+
+ ///
+ /// Constant size in bytes
+ ///
+ public int Size { get; set; }
+
+ ///
+ /// Number of contracts referencing this constant
+ ///
+ public int Refs { get; set; }
+
+ ///
+ /// Account registered this constant
+ ///
+ public Alias Creator { get; set; }
+
+ ///
+ /// Level of the first block baked with this software
+ ///
+ public int CreationLevel { get; set; }
+
+ ///
+ /// Datetime of the first block baked with this software
+ ///
+ public DateTime CreationTime { get; set; }
+
+ ///
+ /// Offchain metadata
+ ///
+ public RawJson Metadata { get; set; }
+ }
+}
diff --git a/Tzkt.Api/Parameters/ExpressionParameter.cs b/Tzkt.Api/Parameters/ExpressionParameter.cs
index f79946fcb..39beda225 100644
--- a/Tzkt.Api/Parameters/ExpressionParameter.cs
+++ b/Tzkt.Api/Parameters/ExpressionParameter.cs
@@ -37,5 +37,9 @@ public class ExpressionParameter
/// Example: `?address.ni=expr1,expr2`.
///
public List Ni { get; set; }
+
+ #region operators
+ public static implicit operator ExpressionParameter(string value) => new() { Eq = value };
+ #endregion
}
}
diff --git a/Tzkt.Api/Repositories/ConstantsRepository.cs b/Tzkt.Api/Repositories/ConstantsRepository.cs
new file mode 100644
index 000000000..79aa3fe8a
--- /dev/null
+++ b/Tzkt.Api/Repositories/ConstantsRepository.cs
@@ -0,0 +1,259 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+using Dapper;
+using Netezos.Encoding;
+
+using Tzkt.Api.Models;
+using Tzkt.Api.Services.Cache;
+
+namespace Tzkt.Api.Repositories
+{
+ public class ConstantsRepository : DbConnection
+ {
+ readonly AccountsCache Accounts;
+ readonly TimeCache Time;
+
+ public ConstantsRepository(AccountsCache accounts, TimeCache time, IConfiguration config) : base(config)
+ {
+ Accounts = accounts;
+ Time = time;
+ }
+
+ public async Task GetCount()
+ {
+ using var db = GetConnection();
+ return await db.QueryFirstAsync(@"SELECT COUNT(*) FROM ""RegisterConstantOps"" WHERE ""Address"" IS NOT NULL");
+ }
+
+ public async Task> Get(
+ ExpressionParameter address,
+ Int32Parameter creationLevel,
+ TimestampParameter creationTime,
+ AccountParameter creator,
+ Int32Parameter refs,
+ Int32Parameter size,
+ SortParameter sort,
+ OffsetParameter offset,
+ int limit)
+ {
+ var sql = new SqlBuilder(@"SELECT * FROM ""RegisterConstantOps""")
+ .Filter(@"""Address"" IS NOT NULL")
+ .Filter("Address", address)
+ .Filter("Level", creationLevel)
+ .Filter("Level", creationTime)
+ .Filter("SenderId", creator)
+ .Filter("Refs", refs)
+ .Filter("StorageUsed", size)
+ .Take(sort, offset, limit, x => x switch
+ {
+ "creationLevel" => ("Level", "Level"),
+ "size" => ("StorageUsed", "StorageUsed"),
+ "refs" => ("Refs", "Refs"),
+ _ => ("Id", "Id")
+ });
+
+ using var db = GetConnection();
+ var rows = await db.QueryAsync(sql.Query, sql.Params);
+
+ return rows.Select(row => new Constant
+ {
+ Address = row.Address,
+ CreationLevel = row.Level,
+ CreationTime = row.Timestamp,
+ Creator = Accounts.GetAlias(row.SenderId),
+ Refs = row.Refs,
+ Size = row.StorageUsed,
+ Value = Micheline.FromBytes(row.Value),
+ Metadata = row.Metadata
+ });
+ }
+
+ public async Task